diff --git a/.gitignore b/.gitignore index 07ad212d..cef22c10 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ tetris.lst tetris.lbl tetris.map tetris.dbg +src/magicnumbers.asm release/* target .env diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..b5651b0d --- /dev/null +++ b/TODO.md @@ -0,0 +1,34 @@ +# Pending +* lowstack should be option instead of mode +* rework tests to fit multi mode +* multiple scoreboards +* clear scoreboard individually +* height 6-8 logic can be rearranged to be closer to vanilla +* hz_display, constants, crunch, harddrop & floor tests need adjustment +* try das meter 1 tile narrower and left/right aligned with playfield vs centered +* move secret grade over when 7 digit scoring is enabled +* move nextpiece option to display +* trans/marathon/sxotkl on one page? +* save/load custom palette to/from sram +* add "x100k" to pace modifier label +* cleanup keyboard seed entry code + +# Bugs +* harddrop mode skips events +* garbage + crunch or floor does not work well +* lowstack line doesn't respond to vert mirror flag +* lowstack nope gets mirrored with horiz mirror flag +* game timer continues after b-game end +* linecap lines highbyte is bcd, needs to be converted to binary +* tap quantity mode should ignore floor/crunch +* next piece isn't affected by mirror flags +* initial trt doesn't display when block tool enabled +* checkerboard completion shows wrong message +* linecap bugs (kirby's note) +* vits scoring does not update when harddropping +* reset defaults doesn't consider detected region +* checkerboard tiles need to be distinct from floor/crunch +* floor setup needs to happen after crunch setup +* reset speedtest attributes +* t-spins trainer does not work + diff --git a/assets/screens/lowstack.png b/assets/screens/lowstack.png old mode 100755 new mode 100644 diff --git a/assets/screens/score-hidden.png b/assets/screens/score-hidden.png old mode 100755 new mode 100644 diff --git a/build.js b/build.js old mode 100644 new mode 100755 index f1051ac2..98ef5262 --- a/build.js +++ b/build.js @@ -1,6 +1,9 @@ +#!/usr/bin/env node + const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); +const { writeFileSync } = require("fs"); console.log('TetrisGYM buildscript'); console.time('build'); @@ -23,6 +26,7 @@ if (args.includes('-h')) { -m mapper -a faster aeppoz + press select to end game +-A build anydas -s disable highscores/SRAM -k Famicom Keyboard support -w force WASM compiler @@ -73,11 +77,6 @@ if (args.includes('-a')) { console.log('using fast aeppoz'); } -if (args.includes('-k')) { - compileFlags.push('-D', 'KEYBOARD=1'); - console.log('using Famicom Keyboard support'); -} - if (args.includes('-s')) { compileFlags.push('-D', 'SAVE_HIGHSCORES=0'); console.log('highscore saving disabled'); @@ -97,6 +96,13 @@ if (args.includes('--')) { console.log(); +// build menu +if (!args.includes('-M')) { + console.time('menu'); + require('./src/menu/menu'); + console.timeEnd('menu'); +} + // build / compress nametables console.time('nametables'); @@ -158,8 +164,25 @@ const flags = compileFlags.join(' '); console.time('assemble'); +// embed latest commit id as magic number +const magicNumbers = spawnSync("git", ["log", "-1", "--pretty='%H'"]) + .stdout.toString() + .trim() + .slice(1, 9) + .toUpperCase() + .match(/../g) + .map((b, i) => `HIGH_SCORE_MAGIC${i} = $${b}`); + +const magicNumbersAsm = `; generated by build.js + +${magicNumbers.join("\n")} +`; + +writeFileSync("src/magicnumbers.asm", magicNumbersAsm); + + exec(`${ca65bin} ${flags} -g src/header.asm -o header.o`); -exec(`${ca65bin} ${flags} -l tetris.lst -g src/main.asm -o main.o`); +exec(`${ca65bin} ${flags} -xx -l tetris.lst -g src/main.asm -o main.o`); console.timeEnd('assemble'); diff --git a/src/boot.asm b/src/boot.asm index 707d2ac2..2655234b 100644 --- a/src/boot.asm +++ b/src/boot.asm @@ -19,27 +19,12 @@ @coldBoot: ; zero out config memory lda #$0 - ldx #$A0 + ldx #menuRAMLength @loop: + sta menuRAM-1, x dex - sta menuRAM, x - ; cpx #0 ; dex sets z flag bne @loop - ; default pace to A - lda #$A - sta paceModifier - - lda #$10 - sta dasModifier - - lda #INITIAL_LINECAP_LEVEL - sta linecapLevel - lda #INITIAL_LINECAP_LINES - sta linecapLines - lda #INITIAL_LINECAP_LINES_1 - sta linecapLines+1 - jsr resetScores .if SAVE_HIGHSCORES @@ -47,9 +32,9 @@ beq @noSRAM jsr checkSavedInit jsr copyScoresFromSRAM + jsr copyVarsFromSRAM @noSRAM: .endif - lda #$54 sta initMagic lda #$2D @@ -60,36 +45,35 @@ sta initMagic+3 lda #$4D sta initMagic+4 + + lda #INITIAL_CUSTOM_LEVEL + sta customLevel + @continueWarmBootInit: ldx #$89 stx rng_seed dex stx rng_seed+1 - ldy #$00 - sty PPUSCROLL - ldy #$00 - sty PPUSCROLL - lda #$90 - sta currentPpuCtrl - sta PPUCTRL - lda #$06 - sta PPUMASK + ; only one byte needed to init b_seed + ; b_seed initialized to add entropy for oneThirdPRNG + ; b_seed is overwritten at the beginning of b games with either seed or rng_seed + stx b_seed+1 jsr LE006 jsr updateAudio2 - jsr updateAudioWaitForNmiAndDisablePpuRendering - jsr disableNmi - jsr drawBlackBGPalette - ; instead of clearing vram like the original, blank out the palette - lda #$EF - ldx #$04 - ldy #$04 ; used to be 5, but we dont need to clear 2p playfield - jsr memset_page - jsr waitForVBlankAndEnableNmi - jsr updateAudioWaitForNmiAndResetOamStaging - jsr updateAudioWaitForNmiAndEnablePpuRendering - jsr updateAudioWaitForNmiAndResetOamStaging lda #$00 sta gameModeState sta gameMode lda #$00 sta frameCounter+1 + + jsr pollControllerButtons + ldy #$8 + ; hold select to start in qual mode + lda heldButtons_player1 + and #BUTTON_SELECT + beq @nonQualBoot + lda #1 + sta qualFlag + ldy #0 +@nonQualBoot: + sty classicLevel diff --git a/src/chr/game_tileset.png b/src/chr/game_tileset.png index fee1c5ac..76a77273 100644 Binary files a/src/chr/game_tileset.png and b/src/chr/game_tileset.png differ diff --git a/src/chr/title_menu_tileset.png b/src/chr/title_menu_tileset.png index a693f74c..5db181b2 100644 Binary files a/src/chr/title_menu_tileset.png and b/src/chr/title_menu_tileset.png differ diff --git a/src/constants.asm b/src/constants.asm index f0560c57..b32e218d 100644 --- a/src/constants.asm +++ b/src/constants.asm @@ -1,3 +1,5 @@ +.include "magicnumbers.asm" + .ifndef INES_MAPPER ; is set via ca65 flags INES_MAPPER := 1000 ; 0 (NROM), 1 (MMC1), 3 (CNROM), 4 (MMC3), 5 (MMC5), and 1000 (autodetect 1/3) .endif @@ -11,10 +13,6 @@ SAVE_HIGHSCORES := 1 AUTO_WIN := 0 .endif -.ifndef KEYBOARD -KEYBOARD := 0 -.endif - .ifndef CNROM_OVERRIDE CNROM_OVERRIDE := 0 .endif @@ -26,13 +24,16 @@ NO_SCORING := 0 ; breaks pace NO_SFX := 0 NO_MENU := 0 ALWAYS_CURTAIN := 0 -QUAL_BOOT := 0 SWAP_DUTY_CYCLES := 0 ; counters the duty cycle swap present in some clone consoles INITIAL_CUSTOM_LEVEL := 29 INITIAL_LINECAP_LEVEL := 39 -INITIAL_LINECAP_LINES := $30 ; bcd -INITIAL_LINECAP_LINES_1 := 3 ; hex (lol) + +; these bytes are currently 2 byte bcd in the menu +; they are swapped and will not work +INITIAL_LINECAP_LINES_LO := $30 ; bcd +INITIAL_LINECAP_LINES_HI := $03 ; bcd (tbd) + BTYPE_START_LINES := $25 ; bcd MENU_HIGHLIGHT_COLOR := $12 ; $12 in gym, $16 in original BLOCK_TILES := $7B @@ -56,152 +57,91 @@ BUTTON_SELECT := $20 BUTTON_START := $10 BUTTON_DPAD := BUTTON_UP | BUTTON_DOWN | BUTTON_LEFT | BUTTON_RIGHT -RENDER_LINES = $01 -RENDER_LEVEL = $02 -RENDER_SCORE = $04 -RENDER_DEBUG = $08 -RENDER_HZ = $10 -RENDER_STATS = $40 -RENDER_HIGH_SCORE_LETTER = $80 +RENDER_LINES := $01 +RENDER_LEVEL := $02 +RENDER_SCORE := $04 +RENDER_DEBUG := $08 +RENDER_HZ := $10 +RENDER_STATS := $40 +RENDER_HIGH_SCORE_LETTER := $80 .enum +MODE_DEFAULT MODE_TETRIS MODE_TSPINS -MODE_SEED -MODE_PARITY -MODE_PACE MODE_PRESETS +MODE_STACKING MODE_TYPEB -MODE_FLOOR -MODE_CRUNCH MODE_TAP +MODE_TAPQTY MODE_TRANSITION MODE_MARATHON -MODE_TAPQTY +MODE_DROUGHT MODE_CHECKERBOARD MODE_GARBAGE -MODE_DROUGHT -MODE_DAS MODE_LOWSTACK MODE_KILLX2 -MODE_INVISIBLE -MODE_HARDDROP +MODE_CALIBRATE MODE_SPEED_TEST -MODE_SCORE_DISPLAY -MODE_CRASH -MODE_STRICT -MODE_HZ_DISPLAY -MODE_INPUT_DISPLAY -MODE_DISABLE_FLASH -MODE_DISABLE_PAUSE -MODE_DARK -MODE_GOOFY -MODE_DEBUG -MODE_LINECAP -MODE_DASONLY -MODE_QUAL -MODE_PAL -.if KEYBOARD = 1 -MODE_KEYBOARD -.endif .endenum -.if KEYBOARD = 1 -MODE_QUANTITY = MODE_KEYBOARD + 1 -.else -MODE_QUANTITY = MODE_PAL + 1 -.endif - -MODE_GAME_QUANTITY = MODE_HARDDROP + 1 +.enum +SCORING_CLASSIC +SCORING_LETTERS +SCORING_SEVENDIGIT +SCORING_FLOAT +SCORING_SCORECAP +SCORING_HIDDEN +.endenum -SCORING_CLASSIC := 0 ; for scoringModifier -SCORING_LETTERS := 1 -SCORING_SEVENDIGIT := 2 -SCORING_FLOAT := 3 -SCORING_SCORECAP := 4 -SCORING_HIDDEN := 5 +.enum +LINECAP_INACTIVE +LINECAP_KILLX2 +LINECAP_FLOOR +LINECAP_INVISIBLE +LINECAP_HALT +.endenum -LINECAP_KILLX2 := 1 -LINECAP_FLOOR := 2 -LINECAP_INVISIBLE := 3 -LINECAP_HALT := 4 +.enum +LINECAP_OFF +LINECAP_LEVEL +LINECAP_LINES +.endenum -CRASH_OFF := 0 -CRASH_SHOW := 1 -CRASH_TOPOUT := 2 -CRASH_CRASH := 3 +.enum +CRASH_OFF +CRASH_SHOW +CRASH_TOPOUT +CRASH_CRASH +.endenum -LINECAP_WHEN_STRING_OFFSET := $10 -LINECAP_HOW_STRING_OFFSET := $12 +LINECAP_WHEN_STRING_OFFSET := $FF +LINECAP_HOW_STRING_OFFSET := $2 MENU_SPRITE_Y_BASE := $46 MENU_MAX_Y_SCROLL := $A0 MENU_TOP_MARGIN_SCROLL := 7 ; in blocks -; menuConfigSizeLookup -; menu ram is defined at menuRAM in ./ram.asm -.macro MENUSIZES - .byte $0 ; MODE_TETRIS - .byte $0 ; MODE_TSPINS - .byte $0 ; MODE_SEED - .byte $0 ; MODE_PARITY - .byte $F ; MODE_PACE - .byte $7 ; MODE_PRESETS - .byte $8 ; MODE_TYPEB - .byte $C ; MODE_FLOOR - .byte $F ; MODE_CRUNCH - .byte $20 ; MODE_TAP - .byte $10 ; MODE_TRANSITION - .byte $4 ; MODE_MARATHON - .byte $1F ; MODE_TAPQTY - .byte $8 ; MODE_CHECKERBOARD - .byte $4 ; MODE_GARBAGE - .byte $12 ; MODE_DROUGHT - .byte $10 ; MODE_DAS - .byte $12 ; MODE_LOWSTACK - .byte $0 ; MODE_KILLX2 - .byte $0 ; MODE_INVISIBLE - .byte $0 ; MODE_HARDDROP - .byte $0 ; MODE_SPEED_TEST - .byte $5 ; MODE_SCORE_DISPLAY - .byte $3 ; MODE_CRASH - .byte $1 ; MODE_STRICT - .byte $1 ; MODE_HZ_DISPLAY - .byte $1 ; MODE_INPUT_DISPLAY - .byte $1 ; MODE_DISABLE_FLASH - .byte $1 ; MODE_DISABLE_PAUSE - .byte $5 ; MODE_DARK - .byte $1 ; MODE_GOOFY - .byte $1 ; MODE_DEBUG - .byte $1 ; MODE_LINECAP - .byte $1 ; MODE_DASONLY - .byte $1 ; MODE_QUAL - .byte $1 ; MODE_PAL -.if KEYBOARD = 1 - .byte $1 ; MODE_KEYBOARD -.endif -.endmacro +NTSC_DAS = 16 +NTSC_ARR = 6 + +PAL_DAS = 12 +PAL_ARR = 4 .macro MODENAMES .byte "TETRIS" .byte "TSPINS" - .byte " SEED " - .byte "STACKN" - .byte " PACE " .byte "SETUPS" + .byte "STACKN" .byte "B-TYPE" - .byte "FLOOR " - .byte "CRUNCH" .byte "QCKTAP" + .byte "TAPQTY" .byte "TRNSTN" .byte "MARTHN" - .byte "TAPQTY" + .byte "LOBARS" .byte "CKRBRD" .byte "GARBGE" - .byte "LOBARS" - .byte "DASDLY" .byte "LOWSTK" .byte "KILLX2" - .byte "INVZBL" - .byte "HRDDRP" + .byte " TEST " .endmacro diff --git a/src/data/mult_orient.asm b/src/data/mult_orient.asm index a1bcd73f..a65c0aba 100644 --- a/src/data/mult_orient.asm +++ b/src/data/mult_orient.asm @@ -29,8 +29,31 @@ multBy100Table: ; 10 spawnTable: ; 7 .byte $02,$07,$08,$0A,$0B,$0E,$12 +.enum +PIECE_T_UP +PIECE_T_RIGHT +PIECE_T_DOWN +PIECE_T_LEFT +PIECE_J_LEFT +PIECE_J_UP +PIECE_J_RIGHT +PIECE_J_DOWN +PIECE_Z_HORIZ +PIECE_Z_VERT +PIECE_O +PIECE_S_HORIZ +PIECE_S_VERT +PIECE_L_RIGHT +PIECE_L_DOWN +PIECE_L_LEFT +PIECE_L_UP +PIECE_I_VERT +PIECE_I_HORIZ +PIECE_SPLIT_SQUARE +PIECE_HIDDEN +.endenum -tetriminoTypeFromOrientation: ; 19 +tetriminoTypeFromOrientation: ; 20 .byte $00,$00,$00,$00 ; t .byte $01,$01,$01,$01 ; j .byte $02,$02 ; z @@ -38,8 +61,9 @@ tetriminoTypeFromOrientation: ; 19 .byte $04,$04 ; s .byte $05,$05,$05,$05 ; l .byte $06,$06 ; i + .byte $03 ; split square -tetriminoTileFromOrientation: ; 20 +tetriminoTileFromOrientation: ; 21 .byte $7B,$7B,$7B,$7B ; t .byte $7D,$7D,$7D,$7D ; j .byte $7C,$7C ; z @@ -47,9 +71,10 @@ tetriminoTileFromOrientation: ; 20 .byte $7D,$7D ; s .byte $7C,$7C,$7C,$7C ; l .byte $7B,$7B ; i + .byte $7B ; split square .byte $FF ; hidden -orientationTableY: ; 80 +orientationTableY: ; 84 .byte $00,$00,$00,$FF ; $00 t up .byte $FF,$00,$00,$01 ; $01 t right .byte $00,$00,$00,$01 ; $02 t down @@ -69,9 +94,10 @@ orientationTableY: ; 80 .byte $FF,$00,$00,$00 ; $10 l up .byte $FE,$FF,$00,$01 ; $11 i vertical .byte $00,$00,$00,$00 ; $12 i horizontal - .byte $00,$00,$00,$00 ; $13 hidden + .byte $00,$00,$01,$01 ; $13 split square + .byte $00,$00,$00,$00 ; $14 hidden -orientationTableX: ; 80 +orientationTableX: ; 84 .byte $FF,$00,$01,$00 ; $00 t up .byte $00,$00,$01,$00 ; $01 t right .byte $FF,$00,$01,$00 ; $02 t down @@ -91,12 +117,8 @@ orientationTableX: ; 80 .byte $01,$FF,$00,$01 ; $10 l up .byte $00,$00,$00,$00 ; $11 i vertical .byte $FE,$FF,$00,$01 ; $12 i horizontal - .byte $00,$00,$00,$00 ; $13 hidden - -; unused. padding required for mult10Tail -.repeat 10 - .byte $00 -.endrepeat + .byte $FF,$01,$FF,$01 ; $13 split square + .byte $00,$00,$00,$00 ; $14 hidden ; needs to be last table in this page mult10Tail: ; 2 diff --git a/src/gamemode/bootscreen.asm b/src/gamemode/bootscreen.asm index 9d4fcdb3..fb02feb4 100644 --- a/src/gamemode/bootscreen.asm +++ b/src/gamemode/bootscreen.asm @@ -2,43 +2,18 @@ gameMode_bootScreen: ; boot ; ABSS goes to gameTypeMenu instead of here ; reset cursors - lda #$0 + lda #MODE_TETRIS sta practiseType - sta menuSeedCursorIndex ; levelMenu stuff + lda #0 sta levelControlMode - lda #INITIAL_CUSTOM_LEVEL - sta customLevel ; detect region jsr updateAudioAndWaitForNmi jsr checkRegion -.if KEYBOARD = 1 - jsr detectKeyboard -.endif - -.if !QUAL_BOOT - ; check if qualMode is already set - lda qualFlag - bne @qualBoot - ; hold select to start in qual mode - lda heldButtons_player1 - and #BUTTON_SELECT - beq @nonQualBoot -.endif -@qualBoot: lda #1 sta gameMode - lda #1 - sta qualFlag + jsr detectKeyboard jmp gameMode_waitScreen - -@nonQualBoot: - ; set start level to 8/18 - lda #$8 - sta classicLevel - lda #2 - sta gameMode - rts diff --git a/src/gamemode/branch.asm b/src/gamemode/branch.asm index b3111a1c..99b1d503 100644 --- a/src/gamemode/branch.asm +++ b/src/gamemode/branch.asm @@ -8,11 +8,26 @@ branchOnGameMode: gameMode_playAndEndingHighScore_jmp, \ gameMode_playAndEndingHighScore_jmp, \ gameMode_playAndEndingHighScore_jmp, \ - gameMode_speedTest + gameMode_speedTest, \ + gameMode_calibrate + +.enum +GAMEMODE_BOOTSCREEN +GAMEMODE_WAITSCREEN +GAMEMODE_GAMETYPEMENU +GAMEMODE_LEVELMENU +GAMEMODE_PLAY +GAMEMODE_UNUSED1 +GAMEMODE_UNUSED2 +GAMEMODE_SPEEDTEST +GAMEMODE_CALIBRATE +.endenum + + .include "bootscreen.asm" .include "waitscreen.asm" -.include "gametypemenu/menu.asm" +.include "../menu/menu.asm" .include "levelmenu.asm" gameMode_playAndEndingHighScore_jmp: diff --git a/src/gamemode/gametypemenu/linecap.asm b/src/gamemode/gametypemenu/linecap.asm deleted file mode 100644 index e112f078..00000000 --- a/src/gamemode/gametypemenu/linecap.asm +++ /dev/null @@ -1,235 +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: - branchTo linecapCursorIndex, \ - linecapMenuControlsWhen, \ - linecapMenuControlsLinesLevel, \ - 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 deleted file mode 100644 index f4fb67b1..00000000 --- a/src/gamemode/gametypemenu/menu.asm +++ /dev/null @@ -1,651 +0,0 @@ -.include "linecap.asm" - -gameMode_gameTypeMenu: -.if NO_MENU - 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 -.if INES_MAPPER <> 0 - lda #CHRBankSet0 - jsr changeCHRBanks -.endif - lda #NMIEnable - sta currentPpuCtrl - jsr waitForVBlankAndEnableNmi - jsr updateAudioWaitForNmiAndResetOamStaging - jsr updateAudioWaitForNmiAndEnablePpuRendering - jsr updateAudioWaitForNmiAndResetOamStaging - -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 - -.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 -.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 -@loop: - ldy menuRAMCounter ; gap support - - ; handle boolean - lda menuCounter - tax ; used to get loaded into Y - lda menuConfigSizeLookup, x - - beq @loopNext ; gap support - inc menuRAMCounter ; only increment RAM when config size isnt zero - - cmp #1 - beq @renderBool - - lda menuCounter - cmp #MODE_SCORE_DISPLAY - beq @renderScoreName - - cmp #MODE_CRASH - beq @renderCrashMode - - cmp #MODE_DARK - beq @renderDarkMode - - jsr menuItemY16Offset - bne @loopNext - txa - - ldx oamStagingLength - - sta oamStaging, x - inx - lda menuVars, y - sta oamStaging, x - inx - lda #$00 - sta oamStaging, x - inx - lda #$E0 - sta oamStaging, x - inx - ; increase OAM index - lda #$04 - clc - adc oamStagingLength - sta oamStagingLength - -@loopNext: - inc menuCounter - lda menuCounter - cmp #MODE_QUANTITY - bne @loop - rts - -@renderBool: - lda menuCounter - jsr menuItemY16Offset - bne @boolOutOfRange - stx spriteYOffset - lda #$E9 - sta spriteXOffset - clc - lda menuVars, y - adc #$8 - sta spriteIndexInOamContentLookup - jsr stringSpriteAlignRight -@boolOutOfRange: - jmp @loopNext - -@renderScoreName: - lda scoringModifier - sta spriteIndexInOamContentLookup - lda #(MODE_SCORE_DISPLAY*8) + MENU_SPRITE_Y_BASE + 1 - jmp @renderOption - -@renderCrashMode: - jsr menuItemY16Offset - bne @loopNext - ldx crashModifier - lda crashOptions, x - sta spriteIndexInOamContentLookup - lda #(MODE_CRASH*8) + MENU_SPRITE_Y_BASE + 1 - jmp @renderOption - -@renderDarkMode: - jsr menuItemY16Offset - bne @loopNext - ldx darkModifier - lda darkOptions, x - sta spriteIndexInOamContentLookup - lda #<((MODE_DARK*8) + MENU_SPRITE_Y_BASE + 1) - -@renderOption: - sec - sbc menuScrollY - sta spriteYOffset - lda #$e9 - sta spriteXOffset - jsr stringSpriteAlignRight - jmp @loopNext - -crashOptions: - .byte $8, $16, $17, $18 - -darkOptions: - .byte $8, $9, $1B, $1C, $1D, $1e - -; <- menu item index in A -; -> 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 diff --git a/src/gamemode/levelmenu.asm b/src/gamemode/levelmenu.asm index 751b4810..92a9e0e8 100644 --- a/src/gamemode/levelmenu.asm +++ b/src/gamemode/levelmenu.asm @@ -1,37 +1,42 @@ gameMode_levelMenu: - lda #NMIEnable - sta currentPpuCtrl - jsr updateAudio2 - lda #$7 - sta renderMode - jsr updateAudioWaitForNmiAndDisablePpuRendering - jsr disableNmi + jsr hideSpritesAndBackground .if INES_MAPPER <> 0 lda #CHRBankSet0 jsr changeCHRBanks .endif - jsr bulkCopyToPpu - .addr menu_palette + stagePatchThenWaitForNmi menuPalette + + ldx #RLE_NT_LEVEL_MENU jsr copyRleNametableToPpu - .addr level_menu_nametable + lda #$20 sta tmp1 lda #$96 ; $6D is OEM position sta tmp2 jsr displayModeText jsr showHighScores - lda linecapFlag + lda linecapWhen beq @noLinecapInfo jsr levelMenuLinecapInfo @noLinecapInfo: + jsr checkIfSeeded + ; patch if seeded + ldy #$20 + ldx #$B6 + jsr patchSeed + ; render lines when loading screen lda #RENDER_LINES sta renderFlags + +; reenable display jsr resetScroll - jsr waitForVBlankAndEnableNmi - jsr updateAudioWaitForNmiAndResetOamStaging - jsr updateAudioWaitForNmiAndEnablePpuRendering - jsr updateAudioWaitForNmiAndResetOamStaging + lda #NMIEnable + sta currentPpuCtrl + lda #RENDER_LEVEL_MENU + sta renderMode + jsr showSpriteAndBackground + lda #$00 sta originalY sta dropSpeed @@ -44,26 +49,41 @@ gameMode_levelMenu: sta classicLevel jmp @forceStartLevelToRange +linecapWhenStrings: + .word STR_LEVEL + .word STR_LINES + +linecapHowStrings: + .word STR_KS2 + .word STR_FLOOR + .word STR_INVIZ + .word STR_HALT + levelMenuLinecapInfo: lda #$20 sta PPUADDR lda #$F5 sta PPUADDR - clc - lda #LINECAP_WHEN_STRING_OFFSET - adc linecapWhen - sta stringIndexLookup - jsr stringBackground + lda linecapWhen + asl + tay + ; use offset, linecapWhen will be 1 or 2, never 0 + ldx linecapWhenStrings-1,y + lda linecapWhenStrings-2,y + tay + jsr stringBackgroundXY lda #$21 sta PPUADDR lda #$15 sta PPUADDR - clc - lda #LINECAP_HOW_STRING_OFFSET - adc linecapHow - sta stringIndexLookup - jsr stringBackground + lda linecapHow + asl + tay + ldx linecapHowStrings+1,y + lda linecapHowStrings+0,y + tay + jsr stringBackgroundXY lda #$20 sta PPUADDR @@ -72,6 +92,7 @@ levelMenuLinecapInfo: jsr render_linecap_level_lines rts + gameMode_levelMenu_processPlayer1Navigation: ; this copying is an artefact of the original lda newlyPressedButtons_player1 @@ -132,12 +153,9 @@ levelMenuCheckStartGame: ldy practiseType cpy #MODE_MARATHON bne @noLevelModification - ldy marathonModifier - cpy #2 ; marathon modes 2 & 4 starts at level 0 - beq @startAtZero - cpy #4 + ldy marathonLevelModifier + cpy #2 bne @noLevelModification -@startAtZero: lda #0 @noLevelModification: sta levelNumber @@ -201,9 +219,9 @@ levelControlClearHighScores: sta spriteXOffset lda #$C8 sta spriteYOffset - lda #$C - sta spriteIndexInOamContentLookup - jsr stringSprite + ldx #>STR_CLEAR + ldy #STR_SURE + ldy # 0 lda #CHRBankSet0 jsr changeCHRBanks .endif - jsr waitForVBlankAndEnableNmi - jsr updateAudioWaitForNmiAndResetOamStaging - jsr updateAudioWaitForNmiAndEnablePpuRendering - jsr updateAudioWaitForNmiAndResetOamStaging +; reenable display + lda #$B0 + sta ppuScrollX + lda #$0 + sta ppuScrollY + lda #NMIEnable|BGPattern1|SpritePattern1 + sta currentPpuCtrl + lda #RENDER_SPEED_TEST + sta renderMode + jsr showSpriteAndBackground @loop: lda heldButtons_player1 @@ -48,6 +48,11 @@ gameMode_speedTest: jmp @loop @back: + lda #RENDER_IDLE + sta renderMode + lda currentPpuMask + and #$E7 + sta PPUMASK lda #$02 sta soundEffectSlot1Init sta gameMode diff --git a/src/gamemode/waitscreen.asm b/src/gamemode/waitscreen.asm index 5b956d64..b6811c9b 100644 --- a/src/gamemode/waitscreen.asm +++ b/src/gamemode/waitscreen.asm @@ -1,42 +1,29 @@ gameMode_waitScreen: lda #0 sta screenStage -waitScreenLoad: - lda #$0 - sta renderMode - jsr updateAudioWaitForNmiAndDisablePpuRendering - jsr disableNmi - lda #NMIEnable - sta currentPpuCtrl + jsr hideSpritesAndBackground .if INES_MAPPER <> 0 ; NROM (and possibly FDS in the future) won't load the 2nd bankset ; and will instead use the title/menu chrset letters. This won't be noticeable -; unless a graphic is added +; unless a graphic is added lda #CHRBankSet1 jsr changeCHRBanks .endif - jsr bulkCopyToPpu - .addr wait_palette - jsr copyRleNametableToPpu - .addr legal_nametable + stagePatchThenWaitForNmi waitPalettePatch - lda screenStage - cmp #2 - bne @justLegal - jsr bulkCopyToPpu - .addr title_nametable_patch -@justLegal: + ldx #RLE_NT_LEGAL + jsr copyRleNametableToPpu - jsr waitForVBlankAndEnableNmi - jsr updateAudioWaitForNmiAndResetOamStaging - jsr updateAudioWaitForNmiAndEnablePpuRendering - jsr updateAudioWaitForNmiAndResetOamStaging +; reenable display + jsr resetScroll + lda #NMIEnable + sta currentPpuCtrl + lda #RENDER_IDLE + sta renderMode + jsr showSpriteAndBackground - ; if title, skip wait - lda screenStage cmp #2 beq waitLoopCheckStart - lda #$FF ldx palFlag ; cpx #0 ; ldx sets z flag @@ -63,10 +50,20 @@ waitScreenLoad: lda #1 sta byteSpriteLen jsr byteSprite + + lda qualFlag + beq @checkStart + jsr showQualWait + jmp @checkSleepCounter +@checkStart: + lda newlyPressedButtons_player1 + and #BUTTON_START + bne @exitLoop +@checkSleepCounter: lda sleepCounter bne @loop +@exitLoop: inc screenStage - jmp @justLegal waitLoopCheckStart: lda screenStage @@ -87,8 +84,27 @@ waitLoopNext: beq waitLoopContinue stx soundEffectSlot1Init inc screenStage - jmp waitScreenLoad + stagePatchThenWaitForNmi titleNametablePatch + jmp waitLoopCheckStart waitLoopContinue: stx soundEffectSlot1Init inc gameMode rts + +showQualWait: + lda heldButtons_player1 + and #BUTTON_START + beq @ret + + lda #$70 + sta spriteXOffset + lda #$80 + sta spriteYOffset + lda #$01 + sta stringAttrib + ldx #>STR_WAIT + ldy # 0 lda #CHRBankSet0 jsr changeCHRBanks .endif - jsr bulkCopyToPpu - .addr game_palette + stagePatchThenWaitForNmi gamePalette + + ldx #RLE_NT_GAME jsr copyRleNametableToPpu - .addr game_nametable + jsr scoringBackground + lda trtFlag + beq @noTrtPatch + stagePatch trtNametable +@noTrtPatch: + + lda dasMeterFlag + beq @noDasMeter + stagePatch dasMeterNametable +@noDasMeter: + jsr debugNametableUI + ldy #$20 + ldx #$A3 + jsr patchSeed + ldy darkModifier beq @notDarkMode + + ; skip NMI tasks during darkmode setup + lda #RENDER_DISABLE + sta renderMode jsr drawDarkMode @notDarkMode: + lda splitSquareFlag + beq @noSplitSquares + + stagePatch splitSquareNametable +@noSplitSquares: + lda hzFlag beq @noHz - jsr bulkCopyToPpu - .addr hzStats + stagePatch hzStats @noHz: +; flush queue here + lda #RENDER_QUEUE + sta renderMode + jsr updateAudioWaitForNmiAndResetOamStaging + lda #RENDER_DISABLE + sta renderMode + lda #$20 sta tmp1 lda #$83 @@ -45,14 +76,14 @@ gameModeState_initGameBackground: sta PPUDATA @heartEnd: +; reenable display + jsr resetScroll lda #NMIEnable|BGPattern1|SpritePattern1 - sta PPUCTRL sta currentPpuCtrl - jsr resetScroll - jsr waitForVBlankAndEnableNmi - jsr updateAudioWaitForNmiAndResetOamStaging - jsr updateAudioWaitForNmiAndEnablePpuRendering - jsr updateAudioWaitForNmiAndResetOamStaging + lda #RENDER_PLAY + sta renderMode + jsr showSpriteAndBackground + lda #$01 sta playState inc gameModeState ; 1 @@ -92,8 +123,7 @@ scoringBackground: ; 7 digit cmp #SCORING_SEVENDIGIT bne @noSevenDigit - jsr bulkCopyToPpu - .addr seven_digit_nametable + stagePatch sevenDigitNametable @noSevenDigit: @@ -143,8 +173,7 @@ MODENAMES debugNametableUI: lda debugFlag beq @notDebug - jsr bulkCopyToPpu - .addr savestate_nametable + stagePatchThenWaitForNmi savestateNametable jsr saveSlotNametablePatch @notDebug: rts @@ -178,52 +207,68 @@ statisticsNametablePatch: rts showPaceDiffText: - lda practiseType - cmp #MODE_PACE - bne @done - jsr bulkCopyToPpu - .addr paceDiffText + lda paceModifier + bmi @done + stagePatch paceDiffText lda #0 @done: rts paceDiffText: ; stripe - .byte $20, $98, $4, $D, $12, $F, $F, $FF + .byte $20, $98, $3, $D, $12, $F, $F, $0 hzStats: ; stripe - .byte $21, $63, $43, $FF - .byte $21, $83, $46, $FF - .byte $21, $C3, $46, $FF - .byte $21, $E3, $42, $FF - .byte $22, $03, $46, $FF - .byte $22, $03, $46, $FF - .byte $22, $43, $46, $FF - .byte $22, $83, $46, $FF - .byte $22, $c3, $46, $FF - .byte $21, $A8, $1, $EC ; hz - .byte $21, $A5, $1, $ED ; . - .byte $23, $D8, $2, $B7, $25 ; hz palette - .byte $22, $23, $3, $1D, $A, $19 ; tap - .byte $22, $63, $3, $D, $15, $22 ; dly - .byte $22, $A3, $3, $D, $12, $1B ; dir - .byte $FF - -seven_digit_nametable: - .byte $20, $5F, $41, $75 ; - - .byte $20, $7f, $C7, $36 ; | - .byte $21, $5F, $41, $77 ; - - .byte $20, $7E, $C7, $FF ; | - .byte $20, $5E, $41, $34 ; - - .byte $21, $5E, $41, $37 ; - - .byte $21, $1E, $41, $0 ; 0 - .byte $FF - -savestate_nametable: - .byte $22,$F7,$8,$74,$34,$34,$34,$34,$34,$34,$75 - .byte $23,$17,$8,$35,$1C,$15,$18,$1D,$FF,$FF,$36 - .byte $23,$37,$8,$35,$FF,$FF,$FF,$FF,$FF,$FF,$36 - .byte $23,$57,$8,$76,$37,$37,$37,$37,$37,$37,$77 - .byte $FF + .byte $21, $63, $2, $FF, $FF, $FF + .byte $21, $83, $5, $FF, $FF, $FF, $FF, $FF, $FF + .byte $21, $C3, $5, $FF, $FF, $FF, $FF, $FF, $FF + .byte $21, $E3, $1, $FF, $FF + .byte $22, $03, $5, $FF, $FF, $FF, $FF, $FF, $FF + .byte $22, $03, $5, $FF, $FF, $FF, $FF, $FF, $FF + .byte $22, $43, $5, $FF, $FF, $FF, $FF, $FF, $FF + .byte $22, $83, $5, $FF, $FF, $FF, $FF, $FF, $FF + .byte $22, $c3, $5, $FF, $FF, $FF, $FF, $FF, $FF + .byte $21, $A8, $0, $EC ; hz + .byte $21, $A5, $0, $ED ; . + .byte $23, $D8, $1, $B7, $25 ; hz palette + .byte $22, $23, $2, $1D, $A, $19 ; tap + .byte $22, $63, $2, $D, $15, $22 ; dly + .byte $22, $A3, $2, $D, $12, $1B ; dir + .byte $0 + +sevenDigitNametable: + .byte $20, $5E, $1, $34, $75 ; - + .byte $20, $7E, $1, $FF, $36 ; | + .byte $20, $9E, $1, $FF, $36 ; | + .byte $20, $BE, $1, $FF, $36 ; | + .byte $20, $DE, $1, $FF, $36 ; | + .byte $20, $FE, $1, $FF, $36 ; | + .byte $21, $1E, $1, $00, $36 ; 0 + .byte $21, $3E, $1, $FF, $36 ; | + .byte $21, $5E, $1, $37, $77 ; - + .byte $0 + +trtNametable: + .byte $23,$17,$3,$74,$34,$34,$75 + .byte $23,$37,$3,$35,$00,$00,$36 + .byte $23,$57,$3,$76,$37,$37,$77 + .byte $0 + +splitSquareNametable: + .byte $22,$23,$1,$B0,$B1 + .byte $22,$43,$1,$B2,$B3 + .byte $0 + +savestateNametable: + .byte $22,$F7,$7,$74,$34,$34,$34,$34,$34,$34,$75 + .byte $23,$17,$7,$35,$1C,$15,$18,$1D,$FF,$FF,$36 + .byte $23,$37,$7,$35,$FF,$FF,$FF,$FF,$FF,$FF,$36 + .byte $23,$57,$7,$76,$37,$37,$37,$37,$37,$37,$77 + .byte $0 + +dasMeterNametable: + .byte $23,$6C,$9,$74,$34,$34,$34,$34,$34,$34,$34,$34,$75 + .byte $23,$8C,$9,$76,$37,$37,$37,$37,$37,$37,$37,$37,$77 + .byte $0 NORMAL_CORNER_TILES := $70 DARK_CORNER_TILES := $80 diff --git a/src/gamemodestate/initstate.asm b/src/gamemodestate/initstate.asm index 23ca3bfd..0de2255b 100644 --- a/src/gamemodestate/initstate.asm +++ b/src/gamemodestate/initstate.asm @@ -1,8 +1,5 @@ gameModeState_initGameState: - lda #$EF - ldx #$04 - ldy #$04 - jsr memset_page + jsr clearPlayfield ldx #$0F lda #$00 ; statsByType @@ -33,6 +30,9 @@ gameModeState_initGameState: sta paceSign sta paceResult+1 sta paceResult+2 + sta gameTimer + sta gameTimer+1 + sta gameTimerStop ; misc sta spawnDelay @@ -47,18 +47,23 @@ gameModeState_initGameState: sta invisibleFlag sta currentFloor sta crashState + sta trtLineCounter + sta trtLineCounter+1 + sta trtScratch+5 + sta trtLines + sta trtLines+1 + sta secretGrade + lda nextBoxStart + sta hideNextPiece ; initialize currentFloor if necessary - lda practiseType - cmp #MODE_FLOOR - bne @notFloor lda floorModifier + beq @notFloor sta currentFloor @notFloor: - lda practiseType - cmp #MODE_INVISIBLE - bne @notInvisible + lda invisibleOptionFlag + beq @notInvisible sta invisibleFlag @notInvisible: @@ -87,7 +92,7 @@ gameModeState_initGameState: sta allegro sta holdDownPoints sta spawnID - lda #$03 + lda #RENDER_PLAY sta renderMode ldx #$A0 lda palFlag @@ -102,17 +107,11 @@ gameModeState_initGameState: jsr generateNextPseudorandomNumber jsr chooseNextTetrimino sta nextPiece - - lda practiseType - cmp #MODE_TRANSITION - bne @notTransition jsr transitionModeSetup -@notTransition: - lda practiseType cmp #MODE_TYPEB bne @notTypeB - lda #BTYPE_START_LINES + lda bTypeLines sta lines @notTypeB: @@ -146,6 +145,7 @@ gameModeState_initGameState: jsr hzStart lda #0 sta hzSpawnDelay + jsr initializeTopRowBuffer jsr practiseInitGameState jsr resetScroll @@ -158,79 +158,41 @@ initGameState_return: rts transitionModeSetup: - lda transitionModifier - cmp #$10 ; (SXTOKL compat) + lda transFlag beq initGameState_return - ; set score - rol - rol - rol - rol - sta bcd32+2 lda #0 - sta bcd32 - sta bcd32+1 - sta bcd32+3 - jsr presetScoreFromBCD - - lda levelNumber - cmp #129 ; everything after 128 transitions immediately - bpl initGameState_return + sta factorB24+1 + sta factorB24+2 + sta lines+1 -@addLinesLoop: - ldx #$A - lda lines - sta tmpX - lda lines+1 - sta tmpY -@incrementLines: - inc lines - lda lines - and #$0F - cmp #$0A - bmi @checkTransition - lda lines - clc - adc #$06 + ldx startLines + lda levelDisplayTable,x sta lines - and #$F0 - cmp #$A0 - bcc @checkTransition - lda lines - and #$0F - sta lines - inc lines+1 - -@checkTransition: - lda lines - and #$0F - bne @lineLoop + ldx #4 +@shift: + asl lines + rol lines+1 + dex + bne @shift - lda lines+1 - sta generalCounter2 - lda lines - sta generalCounter - lsr generalCounter2 - ror generalCounter - lsr generalCounter2 - ror generalCounter - lsr generalCounter2 - ror generalCounter - lsr generalCounter2 - ror generalCounter - lda levelNumber - cmp generalCounter - bpl @lineLoop + sta bcd32+0 + lda #<100000 + sta factorA24+0 + lda #>100000 + sta factorA24+1 + lda #^100000 + sta factorA24+2 -@nextLevel: - lda tmpX - sta lines - lda tmpY - sta lines+1 - rts -@lineLoop: dex - bne @incrementLines - jmp @addLinesLoop + lda startScore + sta factorB24 + jsr unsigned_mul24 + lda product24+0 + sta binScore+0 + lda product24+1 + sta binScore+1 + lda product24+2 + sta binScore+2 + jmp setupScoreForRender presetScoreFromBCD: jsr BCD_BIN @@ -240,10 +202,30 @@ presetScoreFromBCD: sta binScore+1 lda binary32+2 sta binScore+2 - jsr setupScoreForRender - rts + jmp setupScoreForRender initPlayfieldForTypeB: +; decide which seed to use + lda typeBSeedFlag + beq @notSeeded + +; seeded + lda b_seed_input + sta b_seed + lda b_seed_input+1 + sta b_seed+1 + jmp @checkModifier + +@notSeeded: + lda rng_seed + sta b_seed + sta b_seed_input + lda rng_seed+1 + sta b_seed+1 + sta b_seed_input+1 + + +@checkModifier: lda typeBModifier cmp #$6 bmi @normalStart @@ -265,9 +247,9 @@ L87E7: lda generalCounter sta vramRow lda #$09 sta generalCounter3 -L87FC: ldx #rng_seed +L87FC: ldx #b_seed jsr generateNextPseudorandomNumber - lda rng_seed + lda b_seed and #$07 tay lda rngTable,y @@ -284,9 +266,9 @@ L87FC: ldx #rng_seed dec generalCounter3 jmp L87FC -L8824: ldx #rng_seed +L8824: ldx #b_seed jsr generateNextPseudorandomNumber - lda rng_seed + lda b_seed and #$0F cmp #$0A bpl L8824 @@ -298,16 +280,7 @@ L8824: ldx #rng_seed tay lda #EMPTY_TILE sta playfield,y -.if KEYBOARD = 1 - ; this can probably be the same whether keyboard or not. - ; the keyboard code adds the keyboard reading right before the oam staging reset. - ; the additional keyboard reading cycles causes the b type setup to crash. - ; Using the wait routine that skips the oam staging reset (and keyboard read) for now and - ; keeping separate until b-type board test is developed. jsr updateAudioAndWaitForNmi -.else - jsr updateAudioWaitForNmiAndResetOamStaging -.endif dec generalCounter bne L87E7 L884A: diff --git a/src/gamemodestate/pause.asm b/src/gamemodestate/pause.asm index 34fcb550..b4455593 100644 --- a/src/gamemodestate/pause.asm +++ b/src/gamemodestate/pause.asm @@ -1,6 +1,6 @@ gameModeState_handlePause: lda renderMode - cmp #$03 + cmp #RENDER_PLAY bne @ret lda newlyPressedButtons_player1 @@ -30,7 +30,7 @@ pause: lda #$16 sta PPUMASK @pauseSetupNotClassic: - lda #$04 ; render_mode_pause + lda #RENDER_PAUSE sta renderMode @pauseSetupPart2: @@ -54,11 +54,14 @@ pause: sta spriteYOffset @pauseLoopCommon: - clc - lda #$A - adc debugFlag - sta spriteIndexInOamContentLookup - jsr stringSprite + ldx #>STR_PAUSE + ldy #STR_BLOCK + ldy # 0 lda #CHRBankSet0 jsr changeCHRBanks .endif - lda #NMIEnable - sta PPUCTRL - sta currentPpuCtrl - jsr bulkCopyToPpu - .addr menu_palette + stagePatchThenWaitForNmi menuPalette + + ldx #RLE_NT_HIGH_SCORE jsr copyRleNametableToPpu - .addr enter_high_score_nametable + jsr showHighScores lda #$21 sta tmp1 lda #$89 sta tmp2 jsr displayModeText - lda #$02 + +; reenable display + jsr resetScroll + lda #NMIEnable + sta currentPpuCtrl + lda #RENDER_CONGRATS sta renderMode - jsr waitForVBlankAndEnableNmi - jsr updateAudioWaitForNmiAndResetOamStaging - jsr updateAudioWaitForNmiAndEnablePpuRendering - jsr updateAudioWaitForNmiAndResetOamStaging + jsr showSpriteAndBackground ldx highScoreEntryRawPos lda highScoreEntryRowOffsetLookup, x @@ -159,13 +156,13 @@ highScoreEntryScreen: asl adc #$20 sta spriteXOffset - lda #$0E - sta spriteIndexInOamContentLookup + lda #SPRITE_HIGHSCORENAMECURSOR + sta spriteIndex lda frameCounter and #$03 bne @flickerStateSelected_checkForStartPressed - lda #$02 - sta spriteIndexInOamContentLookup + lda #SPRITE_BLANK + sta spriteIndex @flickerStateSelected_checkForStartPressed: jsr loadSpriteIntoOamStaging lda newlyPressedButtons_player1 @@ -177,7 +174,6 @@ highScoreEntryScreen: @checkForAOrRightPressed: -.if KEYBOARD = 1 jsr readKbHighScoreEntry bmi @noKeyboardInput beq @nextTile @@ -186,7 +182,6 @@ highScoreEntryScreen: jmp @waitForVBlank @noKeyboardInput: -.endif lda #BUTTON_RIGHT jsr menuThrottle bne @nextTile diff --git a/src/highscores/util.asm b/src/highscores/util.asm index 63787033..2f366e8c 100644 --- a/src/highscores/util.asm +++ b/src/highscores/util.asm @@ -1,26 +1,84 @@ resetScores: - ldx #$0 + ldx #highScoreLength * highScoreQuantity - 1 lda #$0 @initHighScoreTable: - cpx #highScoreLength * highScoreQuantity - beq @continue sta highscores,x - inx - jmp @initHighScoreTable + dex + bpl @initHighScoreTable @continue: rts + +resetMenuVars: + ldx #sramVariableLength -1 + lda #$0 +@loop: + sta menuVars,x + dex + bpl @loop + + lda #$FF + sta paceModifier + + lda #NTSC_DAS + sta dasModifier + lda #NTSC_ARR + sta arrModifier + + lda #MODE_TETRIS + sta practiseType + + lda #INITIAL_LINECAP_LEVEL + sta linecapLevel + lda #INITIAL_LINECAP_LINES_LO + sta linecapLines+1 + lda #INITIAL_LINECAP_LINES_HI + sta linecapLines + + lda #BTYPE_START_LINES + sta bTypeLines + lda #1 + sta tapLeftColumn + lda #8 + sta tapRightColumn + lda #6 + sta practisePiece + lda #5 + sta startScore + lda #12 + sta startLines + +resetVanillaPalette: + ldx #9 +resetPaletteAtX: + ldy #27 +@palette: + lda colorTable0,x + sta customLevel0+0,y + lda colorTable1,x + sta customLevel0+1,y + lda colorTable2,x + sta customLevel0+2,y + dex + dey + dey + dey + bpl @palette + rts + + + .if SAVE_HIGHSCORES detectSRAM: - lda #$37 + lda #HIGH_SCORE_MAGIC0 sta SRAM_hsMagic - lda #$64 + lda #HIGH_SCORE_MAGIC1 sta SRAM_hsMagic+1 lda SRAM_hsMagic - cmp #$37 + cmp #HIGH_SCORE_MAGIC0 bne @noSRAM lda SRAM_hsMagic+1 - cmp #$64 + cmp #HIGH_SCORE_MAGIC1 bne @noSRAM lda #1 rts @@ -30,51 +88,94 @@ detectSRAM: checkSavedInit: lda SRAM_hsMagic+2 - cmp #$4B - bne resetSavedScores + cmp #HIGH_SCORE_MAGIC2 + bne @resetSaved lda SRAM_hsMagic+3 - cmp #$D2 - bne resetSavedScores + cmp #HIGH_SCORE_MAGIC3 + beq @checkMenuVars +@resetSaved: + jsr resetSavedScores +@checkMenuVars: + lda SRAM_varMagic+0 + cmp #HIGH_SCORE_MAGIC0 + bne @resetSavedVars + lda SRAM_varMagic+1 + cmp #HIGH_SCORE_MAGIC1 + bne @resetSavedVars + lda SRAM_varMagic+2 + cmp #HIGH_SCORE_MAGIC2 + bne @resetSavedVars + lda SRAM_varMagic+3 + cmp #HIGH_SCORE_MAGIC3 + beq @ret +@resetSavedVars: + jsr resetSavedVars +@ret: rts resetSavedScores: - lda #$4B + lda #HIGH_SCORE_MAGIC2 sta SRAM_hsMagic+2 - lda #$D2 + lda #HIGH_SCORE_MAGIC3 sta SRAM_hsMagic+3 - ldx #$0 + ldx #highScoreLength * highScoreQuantity - 1 lda #$0 @copyLoop: - cpx #highScoreLength * highScoreQuantity - beq @continue sta SRAM_highscores,x - inx - jmp @copyLoop + dex + bpl @copyLoop + rts + + +resetSavedVars: + lda #HIGH_SCORE_MAGIC0 + sta SRAM_varMagic+0 + lda #HIGH_SCORE_MAGIC1 + sta SRAM_varMagic+1 + lda #HIGH_SCORE_MAGIC2 + sta SRAM_varMagic+2 + lda #HIGH_SCORE_MAGIC3 + sta SRAM_varMagic+3 + jsr resetMenuVars +copyVarsToSram: + ldx #sramVariableLength - 1 +@varsLoop: + lda menuVars,x + sta SRAM_variables,x + dex + bpl @varsLoop @continue: rts + copyScoresFromSRAM: - ldx #$0 + ldx #highScoreLength * highScoreQuantity - 1 @copyLoop: - cpx #highScoreLength * highScoreQuantity - beq @continue lda SRAM_highscores,x sta highscores,x - inx - jmp @copyLoop + dex + bpl @copyLoop @continue: rts copyScoresToSRAM: - ldx #$0 + ldx #highScoreLength * highScoreQuantity - 1 @copyLoop: - cpx #highScoreLength * highScoreQuantity - beq @continue lda highscores,x sta SRAM_highscores,x - inx - jmp @copyLoop + dex + bpl @copyLoop +@continue: + rts + +copyVarsFromSRAM: + ldx #sramVariableLength - 1 +@copyLoop: + lda SRAM_variables,x + sta menuVars,x + dex + bpl @copyLoop @continue: rts diff --git a/src/io.asm b/src/io.asm index 1e99e4da..6c66f4c1 100644 --- a/src/io.asm +++ b/src/io.asm @@ -3,6 +3,9 @@ SRAM_states := SRAM SRAM_hsMagic := SRAM+$A00 SRAM_highscores := SRAM_hsMagic+$4 +SRAM_varMagic := SRAM+$B00 +SRAM_variables := SRAM_varMagic+$4 + PPUCTRL := $2000 PPUMASK := $2001 PPUSTATUS := $2002 diff --git a/src/macros.asm b/src/macros.asm index 8ec5742f..8551ba61 100644 --- a/src/macros.asm +++ b/src/macros.asm @@ -56,3 +56,30 @@ .endif .endscope .endmacro + + + +.macro stagePatch patchAddr + ldx #patchAddr + jsr copyPatchAtXYToQueue +.endmacro + +.macro stagePatchThenDump patchAddr + ldx #patchAddr + jsr copyPatchAtXYToQueue + jsr render_mode_queue +.endmacro + +.macro stagePatchThenWaitForNmi patchAddr +; can be made subroutine if needed to save space + stagePatch patchAddr + lda renderMode + pha + lda #RENDER_QUEUE + sta renderMode + jsr updateAudioWaitForNmiAndResetOamStaging + pla + sta renderMode +.endmacro diff --git a/src/main.asm b/src/main.asm index e240c812..fe62cf03 100644 --- a/src/main.asm +++ b/src/main.asm @@ -16,7 +16,10 @@ .linecont .segment "PRG_chunk1": absolute - +.align $100 +; these tables benefit from page alignment +.include "data/mult_orient.asm" +.include "nmi/render_mode_queue.asm" ; region code at start of page to keep cycle count consistent .include "util/check_region.asm" .include "audio.asm" @@ -32,9 +35,7 @@ mainLoop: .include "nmi/nmi.asm" .include "nmi/render.asm" .include "nmi/pollcontroller.asm" -.if KEYBOARD .include "keyboard/poll.asm" -.endif .include "gamemode/branch.asm" ; -> playAndEnding @@ -87,10 +88,10 @@ mainLoop: .include "modes/crunch.asm" .include "modes/qtap.asm" .include "modes/garbage.asm" - -.align $100 -; these tables benefit from page alignment -.include "data/mult_orient.asm" +.include "seeds.asm" +.include "modes/dasmeter.asm" +.include "modes/calibrate.asm" +.include "modes/secretgrade.asm" .segment "PRG_chunk3": absolute diff --git a/src/menu/.gitignore b/src/menu/.gitignore new file mode 100644 index 00000000..154cd49e --- /dev/null +++ b/src/menu/.gitignore @@ -0,0 +1,2 @@ +menudata.asm +menuram.asm diff --git a/src/menu/menu.asm b/src/menu/menu.asm new file mode 100644 index 00000000..cd5b7dd5 --- /dev/null +++ b/src/menu/menu.asm @@ -0,0 +1,1275 @@ +MENU_VARS_HI = >menuVars +MENU_VARS_PAGE = menuVars & $FF00 + +GAME_ACTIVE = $FF + +; valid background chars are 0-253 +NORAM = $00 + +MENU_TITLE_PPU = $2106 +MENU_STRIPE_WIDTH = 20 +MENU_ROWS = 17 + + +CURSOR_SLEEP_1 = 33 +CURSOR_SLEEP_2 = 44 + + +MENU_STACK = $DF ; $01C8 - $01DF intended range + +; custom routines +.enum +RESET_DEFAULTS +CLEAR_SCOREBOARD +LOAD_VANILLA +LOAD_PRIDE +LOAD_WHITE +LOAD_BUGGED +.endenum + +menuDataStart: +.include "menudata.asm" +.out .sprintf("Menu data: %d", *-menuDataStart) + +; table of first items instead +; + table of item counts + +VALUE_MASK = %00011111 +TYPE_MASK = %11100000 + +; tttnnnnn +TYPE_CUSTOM = %00000000 ; n = custom routine +TYPE_NUMBER = %00100000 ; n = limit +TYPE_CHOICES = %01000000 ; n = wordlist index +TYPE_FF_OFF = %01100000 ; n = limit + +TYPE_UNUSED = %10000000 +TYPE_GAMEMODE = %10100000 ; n = mode +TYPE_DIGIT = %11000000 +TYPE_SUBMENU = %11100000 ; n = menu index + + +BCD_MASK = $10 +TYPE_BCD = TYPE_DIGIT | BCD_MASK +TYPE_HEX = TYPE_DIGIT +DIGIT_VALUE_MASK = $F + +sleepToggle = tetriminoY + +menuCode: + +menuStackPush: + ldx menuStackPtr + sta menuStack,x + inc menuStackPtr + rts + +menuStackPop: + dec menuStackPtr + ldx menuStackPtr + lda menuStack,x + rts + +gameMode_gameTypeMenu: +.if NO_MENU + inc gameMode + rts +.endif + jsr hideSpritesAndBackground + stagePatchThenWaitForNmi titlePalette + + ldx #RLE_NT_GAME_MENU + jsr copyRleNametableToPpu + +.if INES_MAPPER <> 0 + lda #CHRBankSet0 + jsr changeCHRBanks +.endif + +; reenable display + jsr resetScroll + lda #NMIEnable + sta currentPpuCtrl + lda #RENDER_QUEUE + sta renderMode + jsr showSpriteAndBackground + + lda #MENU_VARS_HI + sta byteSpriteAddr+1 + lda #0 + sta byteSpriteTile + sta vramRow + sta gameStarted + sta sleepCounter + sta sleepToggle + jsr makeNotReady + +; check to see if returning from level menu or game + ldy activeMenu + iny + bne @initMenu + jsr exitSubmenuNoSfx + jmp gameTypeLoop +@initMenu: + lda #0 + sta menuStackPtr + jsr enterMenu + +gameTypeLoop: + lda gameStarted + beq @noGame + lda #0 + sta killX2Flag + lda practiseType + cmp #MODE_SPEED_TEST + bne @notSpeedTest + lda #GAMEMODE_SPEEDTEST + sta gameMode + bne @sfx +@notSpeedTest: + lda practiseType ; is already in A, but this is explicit + cmp #MODE_KILLX2 + bne @notKillX2 + lda #RENDER_IDLE + sta renderMode + lda #0 + sta gameModeState + lda #39 + sta levelNumber + lda #1 + sta killX2Flag + inc gameMode +@notKillX2: + lda practiseType + cmp #MODE_CALIBRATE + bne @notCalibrate + lda #0 + sta levelNumber + lda #GAMEMODE_CALIBRATE + sta gameMode + bne @sfx +@notCalibrate: + inc gameMode +@sfx: + lda #$2 + sta soundEffectSlot1Init + rts +@noGame: + ; todo: write down which vars are used by which func + jsr collectControllerInput + jsr setScratch + jsr randomizeSeed + jsr addInputs + jsr checkGoofy + jsr respondToInput + jsr stageCursor + ; scratch is not important anymore + jsr stageVRAMRow + jsr stageVRAMRow + jsr stageVRAMRow + jsr stageVRAMRow + jsr stageVRAMRow + jsr stageVRAMRow + lda goofyFlag + sta prevGoofy +gameTypeLoopWait: + jsr updateAudioWaitForNmiAndResetOamStaging + jmp gameTypeLoop + +.out .sprintf("bg setup & loop: %d", *-gameMode_gameTypeMenu) + + +enterSubMenu: + ldy #$02 + sty soundEffectSlot1Init + pha + lda #$FF + sta vramRow + lda activeRow + jsr menuStackPush + lda activePage + jsr menuStackPush + lda activeMenu + jsr menuStackPush + pla +enterMenu: + sta activeMenu + cmp #GAME_ACTIVE + bne @normalMenu + rts +@normalMenu: + lda #0 +enterPage: + sta activePage + sta originalPage + ldy activeMenu + clc + adc startPageByMenu,y + sta actualPage + jsr setItemCount + lda #$FF + sta vramRow + ldx actualPage + lda pageTypes,x + and #VALUE_MASK + sta unpackedPageValue + beq @noStorePractiseType + sta practiseType + +@noStorePractiseType: + lda pageTypes,x + and #TYPE_MASK + sta unpackedPageType + + ldy activeMenu + 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: + jsr menuStackPop + jsr enterMenu + jsr menuStackPop + jsr enterPage + jsr menuStackPop + sta activeRow + jmp setScratch + + +setupUD: + ldy activeColumn + bne setupUDDigitChange + +setupUDRowChange: +; ud change row 1/2 - activeColumn == 0 + ldy #$00 + lda unpackedPageType + ldx activeMenu + lda pageCountByMenu,x + tax + dex + beq @storeMin ; no page select row for single page + dey +@storeMin: + sty udMin + lda pageItemCount + 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 #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 + txa + asl + tax + lda choiceSetIndexes+1,x + lsr + lsr + lsr + lsr + tax + inx + inx +@storeMax: + stx lrMax + rts + +setupLRColumnChange: +; setupLRColumnChange itemType & %10100000 == %10000000 + lda #0 + sta lrMin + lda #>activeColumn + sta lrPointer+1 + lda #pageLabels + sta @itemPtr+1 + + ldx actualPage + lda pageTypes,x + lsr + lsr + lsr + lsr + lsr + sta @padding + + lda vramRow + asl + tay + + ldx renderQueuePointer + lda menuVramRowTable+1,y + sta stack,x + inx + lda menuVramRowTable,y + sta stack,x + inx + lda #MENU_STRIPE_WIDTH-1 + sta stack,x + inx + +@loop: + lda pageItemCount + cmp vramRow + bcc @fillBlank + + lda (@itemPtr),y + clc + adc #strTable + sta @stringPtr+1 + +; padding goes here + lda vramRow + bne @startString + lda #$FF +@pad: + dec @padding + bmi @startString + sta stack,x + dec @blankCounter + inx + bne @pad +@startString: + + ldy #0 +@copy: + lda (@stringPtr),y + sta stack,x + iny + inx + dec blankCounter + dec stringLength + bpl @copy +@fillBlank: ; should only be entered directly when end of string reached + dec @blankCounter + bmi @finish + lda #$FF + sta stack,x + inx + bne @fillBlank ; always taken +@finish: + inc renderQueueLength + stx renderQueuePointer + + lda vramRow + beq @noValue + lda pageItemCount + cmp vramRow + bcc @noValue + + txa + sec + sbc #8 + sta renderQueuePointer + jsr stageCurrentValue + + +@noValue: + inc vramRow + lda vramRow + cmp #MENU_ROWS + bne @ret2 + lda #$20 + sta vramRow +@ret2: + rts + +stageCurrentValue: + ldx actualPage + lda startItemByPage,x + clc + adc vramRow + sec + sbc #1 + tay + sty activeItem + lda memoryOffsets,y + sta byteSpriteAddr + lda #MENU_VARS_HI + sta byteSpriteAddr+1 + lda itemTypes,y + tax + ldy #0 + and #TYPE_MASK + cmp #TYPE_CUSTOM + bne @notCustom + jmp @ret +@notCustom: + sta unpackedItemType + 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 + asl + tay +@startCopy: + lda (stringSetPtr),y + clc + adc #strTable + sta stringSetPtr+1 + pla + sta stringSetPtr + lda generalCounter + jsr setStackOffset + + ldy #0 +@nextChar: + lda (stringSetPtr),y + sta stack,x + inx + iny + dec generalCounter + bne @nextChar + +@endCopy: + jmp @ret + +@setStringList: + txa + asl + tax + lda choiceSetIndexes,x + clc + adc #choiceSets + sta stringSetPtr+1 + rts + +@digitInputOrEdge: + and #TYPE_MASK + cmp #TYPE_GAMEMODE + beq @ret + cmp #TYPE_SUBMENU + beq @ret + txa + and #DIGIT_VALUE_MASK +@drawOneByte: + pha + sec + sbc #1 + lsr + clc + adc #$1 + sta generalCounter + pla + jsr setStackOffset + + ldy #$00 + sty generalCounter2 + +; skip decimal if 2 digit hex/bcd + lda unpackedItemType + cmp #TYPE_DIGIT + beq @digitLoop + +; decimal conversion if 2 digit hex + lda generalCounter + cmp #1 + bne @digitLoop + lda (byteSpriteAddr),y + cmp #100 + bcc @bcd + inc generalCounter2 + tay + lda #1 + sta stack-1,x + tya + sbc #100 + cmp #100 + bcc @bcd + sbc #100 + inc stack-1,x +@bcd: + tay + lda levelDisplayTable,y + ldy generalCounter2 + bne @oneDigit ; if at least 100, draw 0 in 10s + cmp #10 + bcs @oneDigit + + ; skip 10s + inx + bne @lowNybble + +@digitLoop: ; y is zero + lda (byteSpriteAddr),y +@oneDigit: + pha + lsr + lsr + lsr + lsr + sta stack,x + inx + pla +@lowNybble: + and #$0F + sta stack,x + inx + iny + dec generalCounter + bne @digitLoop +@ret: + + lda renderQueuePointer + clc + adc #8 + sta renderQueuePointer + rts + +setStackOffset: + eor #$FF + clc + adc #$09 + clc + adc renderQueuePointer + tax + rts + +menuVramRowTable: +; 17 for now (title + 16 items) + .addr $2109 + .addr $2146 + .addr $2166 + .addr $2186 + .addr $21A6 + .addr $21C6 + .addr $21E6 + .addr $2206 + .addr $2226 + .addr $2246 + .addr $2266 + .addr $2286 + .addr $22A6 + .addr $22C6 + .addr $22E6 + .addr $2306 + .addr $2326 + + +.out .sprintf("stage row: %d", *-stageVRAMRow) + +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 #SPRITE_MENUPAGESELECTA + sta spriteIndex + jmp @stage + +@notTitle: + asl + asl + asl + clc + adc #$4F + sta spriteYOffset +; digit input + ldx activeColumn + beq @notColumn + txa + asl + asl + asl + clc + adc #$B9 + sta spriteXOffset + ldx activeItem + lda itemTypes,x + and #DIGIT_VALUE_MASK + sec + sbc #1 + lsr + asl + asl + asl + asl + eor #$FF + sec + adc spriteXOffset + sta spriteXOffset + lda #SPRITE_SEEDCURSORA ; digit select + bne @store +@notColumn: + lda #$1A + sta spriteXOffset + +; this code is redundant + ldx activeMenu + lda startPageByMenu,x + clc + adc activePage + tax + lda activeRow + clc + adc startItemByPage,x + tax + lda itemTypes,x + + and #TYPE_MASK + cmp #TYPE_CUSTOM + beq @noValue + cmp #TYPE_SUBMENU + beq @noValue + cmp #TYPE_GAMEMODE + beq @noValue + lda #SPRITE_PRACTISETYPECURSORA ; option select +@store: + sta spriteIndex +@stage: + lda sleepCounter + bne @noToggle + lda sleepToggle + eor #1 + sta sleepToggle + beq @sleep1 + lda #CURSOR_SLEEP_2 + bne @storeSleep +@sleep1: + lda #CURSOR_SLEEP_1 +@storeSleep: + sta sleepCounter +@noToggle: + lda sleepToggle + clc + adc spriteIndex + sta spriteIndex +@noFrameAdjust: + jmp loadSpriteIntoOamStaging +@noValue: + lda #SPRITE_MENUSTARTA + sta spriteIndex + jmp @stage +gotoEdgeCase: + rts + +.out .sprintf("cursor staging: %d", *-stageCursor) + + +.out .sprintf("total: %d", *-menuCode) + +renderQueuePush: + ldx renderQueuePointer + sta stack,x + inc renderQueuePointer + rts diff --git a/src/menu/menu.js b/src/menu/menu.js new file mode 100644 index 00000000..6733ac66 --- /dev/null +++ b/src/menu/menu.js @@ -0,0 +1,391 @@ +const { mainMenu, extraSpriteStrings } = require("./menudata"); +const { writeFileSync } = require("fs"); + +const MAX_LENGTH_NAME = 14; +const MAX_LENGTH_VALUE = 8; + +const labelMap = { + TYPE_BCD: typeDigit, + TYPE_HEX: typeDigit, + TYPE_NUMBER: typeNumber, + TYPE_FF_OFF: typeNumber, + TYPE_CHOICES: typeChoices, + TYPE_GAMEMODE: typeGameMode, + TYPE_SUBMENU: typeSubMenu, + TYPE_BOOL: typeBool, + TYPE_CUSTOM: typeCustom, +}; + +const bssMap = []; +const choiceSetEnums = []; +const choiceSetIndexes = []; +const choiceSets = []; +const items = []; +const memoryMap = []; +const menuEnums = []; +const newWords = new Set(); +const pageCountByMenu = []; +const pageLabels = {}; +const pagesOutput = []; +const startItemByPage = []; +const startPageByMenu = []; +const unlabeledStringSets = {}; + +let index = 0; +let pageIndex = 0; + +function checkStringSanity(string) { + if (string.length > MAX_LENGTH_VALUE) { + throw new Error(`${string} is more than MAX_LENGTH_VALUE chars`); + } + let match; + 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 getStringConstant(word) { + return `str_${cleanWord(word)}`.toUpperCase(); +} + +function getChoiceSetName(word) { + return `choiceSet${cleanWord(word)}`; +} + +function getChoiceSetConstant(name) { + return `CHOICESET_${cleanWord(name).toUpperCase()}`; +} + +function getByteLine(byte) { + return ` .byte ${byte}`; +} + +function getWordLine(word) { + return ` .word ${word}`; +} + +function getHexByte(number) { + if (isNaN(number)) return number; + return `$${number.toString(16).padStart(2, "0").toUpperCase()}`; +} + +function getHexWord(number) { + if (isNaN(number)) return number; + return `$${number.toString(16).padStart(4, "0").toUpperCase()}`; +} + +function getOutputLines(itemType, string, memory, length) { + return { + string: string, + label: getByteLine(`${itemType} ; ${string}`), + memory: memory, // has to be processed separately to get output line + length: length, + }; +} + +function getStringByte(c) { + const 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 getPageLines(title, page) { + let label; + let mode; + [, label, mode] = title.match(/([^[]*)(?:\s*\[mode=(\w+)\])?/i); + const padding = getHexByte( + (Math.round((MAX_LENGTH_NAME - label.length) / 2) << 5) & 0xff, + ); + const modifier = mode ? `MODE_${mode.toUpperCase()}` : "MODE_DEFAULT"; + const pagelabelsName = `pageLabels${cleanWord(label)}`; + newWords.add(label.toUpperCase()); + + if (!pageLabels[`${pagelabelsName}`]) { + pageLabels[`${pagelabelsName}`] = [ + getWordLine(getStringConstant(label)), + ...page.map((p) => getWordLine(getStringConstant(p[1]))), + ]; + } + page.forEach((p) => { + newWords.add(p[1].toUpperCase()); + }); + return { + label: getByteLine(`${padding} | ${modifier} ; ${label}`), + index: getWordLine( + `${getHexWord(page.length << 11)} | (${pagelabelsName} - pageLabels)`, + ), + newsets: `${pagelabelsName}:`, + }; +} + +function typeDigit(label, string, digits, memoryLabel, defaultValue) { + if (digits < 2 || digits > 8 || digits & 1) { + throw new Error(`${string}: digits can only be 2, 4, 6 or 8`); + } + return getOutputLines( + `${label} | ${getHexByte(digits)}`, + string, + memoryLabel, + (digits + 1) >> 1, + ); +} + +function typeChoices(label, string, choiceSet, memoryLabel) { + const stringSet = [...choiceSet] + .map((c) => cleanWord(c.slice(0, 6))) + .join(""); + unlabeledStringSets[stringSet] = choiceSet; + return getOutputLines( + `${label} | ${getChoiceSetConstant(stringSet)}`, + string, + memoryLabel, + 1, + ); +} + +function typeNumber(label, string, limit, memoryLabel, defaultValue) { + return getOutputLines( + `${label} | ${getHexByte(limit)}`, + string, + memoryLabel, + 1, + ); +} + +function typeBool(_, string, memoryLabel) { + return typeChoices("TYPE_CHOICES", string, ["off", "on"], memoryLabel, 1); +} +function typeSubMenu(label, string) { + return getOutputLines( + `${label} | SUBMENU_${cleanWord(string).toUpperCase()}`, + `${string}`, + ); +} + +function typeGameMode(label, string, mode) { + return { + string: string, + label: getByteLine(`${label} | ${mode} ; ${string}`), + memory: 0, + }; +} +function typeCustom(label, string, subroutine, memoryLabel) { + return getOutputLines( + `${label} | ${subroutine}`, + `${string}`, + memoryLabel, + ); +} + +const subMenus = []; +const processPageSet = (pages, name) => { + if (name) { + const enunName = `SUBMENU_${cleanWord(name).toUpperCase()}`; + if (!menuEnums.includes(enunName)) menuEnums.push(enunName); + } + startPageByMenu.push( + `${getByteLine(getHexByte(pageIndex))} ; ${name ? name : "main menu"}`, + ); + // collect submenus to process after all pages + let subPageSets = {}; + Object.entries(pages).forEach(([title, page]) => { + pageIndex++; + startItemByPage.push( + getByteLine(`${getHexByte(index)} ; ${cleanWord(title)}`), + ); + pagesOutput.push(getPageLines(title, page)); + page.forEach((item) => { + const output = labelMap[item[0]](...item); + + const resLine = `${output.memory}: .res ${output.length} ; ${output.string}`; + + if (output.memory && (bssMap.indexOf(resLine) < 0)) + bssMap.push(resLine); + + items.push(output); + index++; + if (item[0] === "TYPE_SUBMENU") { + // submenus are expected to be unique by name + // skip if the submenu has been processed, it already exists + if (subMenus.indexOf(item[1]) < 0) { + subMenus.push(item[1]); + subPageSets[item[1]] = item[2]; + } + } + }); + }); + pageCountByMenu.push( + getByteLine( + `${getHexByte(Object.values(pages).length)} ; ${name ? name : "main menu"}`, + ), + ); + + // process any submenus the same way as the main menu + Object.entries(subPageSets).forEach(([name, pages]) => { + processPageSet(pages, name); + }); +}; +processPageSet(mainMenu); + +items.forEach((i) => { + const line = getByteLine( + `${i.memory ? "<" + i.memory : "NORAM"} ; ${i.string}`, + ); + memoryMap.push(line); +}); + +[ + ["extraSpriteStrings", extraSpriteStrings], + ...Object.entries(unlabeledStringSets), +].forEach(([name, choiceSet]) => { + if (name != "extraSpriteStrings") { + choiceSetEnums.push(getChoiceSetConstant(name)); + choiceSetIndexes.push( + getWordLine( + `${getHexWord((choiceSet.length - 2) << 12)} | (${getChoiceSetName(name)} - choiceSets)`, + ), + ); + choiceSets.push(`${getChoiceSetName(name)}:`); + } + choiceSet.forEach((choice) => { + choice = choice.toLowerCase(); + checkStringSanity(choice); + newWords.add(choice.toUpperCase()); + if (name !== "extraSpriteStrings") { + choiceSets.push( + // getByteLine(`${getStringName(choice)}-${getChoiceSetName(name)}`), + getWordLine(getStringConstant(choice)), + ); + } + }); +}); + +/* + * create a blob of words + */ +let wordTable = ""; +const sortedWords = [...newWords].sort((a, b) => b.length - a.length); + +sortedWords.forEach((w) => { + let index = wordTable.search(RegExp.escape(w)); + index < 0 && (wordTable = wordTable + w); +}); + +if (wordTable.length > 1023) { + throw new Error(`is 1024 bytes of words not enough?`); +} + +function wordConstants() { + return sortedWords.map((w) => { + let index = wordTable.search(RegExp.escape(w)); + return `${getStringConstant(w)} = ${getHexWord(((w.length - 1) << 12) | index)}`; + }); +} + +function wordChunks() { + return wordTable + .match(/.{1,8}/g) + .map((w) => getByteLine(getStringBytes(w))); +} +const menuram = ` +; generated by menu.js +; will be overwritten unless built with -M + + +${bssMap.join("\n")} +`; +const output = ` +; generated by menu.js +; will be overwritten unless built with -M + +.enum +MAIN_MENU +${menuEnums.join("\n")} +MENU_COUNT +.endenum +.out .sprintf("%d/32 menus", MENU_COUNT) + +.enum +${choiceSetEnums.join("\n")} +CHOICESET_COUNT +.endenum +.out .sprintf("%d/32 choicesets", CHOICESET_COUNT) + + +; index activeMenu + +startPageByMenu: +${startPageByMenu.join("\n")} + +pageCountByMenu: +${pageCountByMenu.join("\n")} + +; index activePage +; PPPMMMMM +; P = padding +; M = mode +pageTypes: +${pagesOutput.map((p) => p.label).join("\n")} + +; CCCCCOOO OOOOOOOO +; C = item count +; O = offset from pageIndexes + +pageIndexes: +${pagesOutput.map((p) => p.index).join("\n")} + +pageLabels: +${Object.entries(pageLabels) + .map(([k, v]) => [k + ":", ...v].join("\n")) + .join("\n")} + +startItemByPage: +${startItemByPage.join("\n")} + +; index activeItem + +memoryOffsets: +${memoryMap.join("\n")} + +; TTTVVVVV +; T = type +; V = value +itemTypes: +${items.map((i) => i.label).join("\n")} + +; CCCCOOOO OOOOOOOO +; C = choicecount - 2 +; O = offset from choiceSets +choiceSetIndexes: +${choiceSetIndexes.join("\n")} + +choiceSets: +${choiceSets.join("\n")} + +strTable: +${wordChunks().join("\n")} + +; LLLLOOOO OOOOOOOO +; L = length - 1 +; O = offset from strTable; + +${wordConstants().join("\n")} +`; + +writeFileSync(__dirname + "/menuram.asm", menuram); +writeFileSync(__dirname + "/menudata.asm", output); diff --git a/src/menu/menudata.js b/src/menu/menudata.js new file mode 100644 index 00000000..b1144792 --- /dev/null +++ b/src/menu/menudata.js @@ -0,0 +1,322 @@ +const seedFlag = ["TYPE_BOOL", "Seed Enabled", "seedEnabled"]; +const seedInput = ["TYPE_HEX", "seed", 6, "set_seed_input"]; +const linecapWhen = [ + "TYPE_CHOICES", + "linecap", + ["off", "level", "lines"], + "linecapWhen", +]; +const linecapHow = [ + "TYPE_CHOICES", + "linecap how", + ["ks*2", "floor", "inviz", "halt"], + "linecapHow", +]; +const linecapLevel = ["TYPE_NUMBER", "linecap level", 0, "linecapLevel"]; +const linecapLines = ["TYPE_BCD", "linecap lines", 4, "linecapLines"]; +const dasOnly = ["TYPE_BOOL", "das only", "dasOnlyFlag"]; +const vitsScoreFlag = ["TYPE_BOOL", "vits scoring", "vitsScoreFlag"]; + +const scoringModifier = [ + "TYPE_CHOICES", + "scoring", + ["classic", "letters", "7digit", "m", "capped", "hidden"], + "scoringModifier", +]; +const modernLinesFlag = ["TYPE_BOOL", "modern lines", "modernLinesFlag"]; +const paceModifier = ["TYPE_FF_OFF", "Pace", 16, "paceModifier"]; +const hzFlag = ["TYPE_BOOL", "HZ DISPLAY", "hzFlag"]; +const inputDisplayFlag = ["TYPE_BOOL", "Input Display", "inputDisplayFlag"]; +const disableFlash = ["TYPE_BOOL", "Disable Flash", "disableFlashFlag"]; +const secretGrading = ["TYPE_BOOL", "Secret Grading", "secretGradingFlag"]; +const darkMode = [ + "TYPE_CHOICES", + "dark mode", + ["off", "on", "neon", "lite", "teal", "og"], + "darkModifier", +]; + +const paletteSelection = [ + "TYPE_CHOICES", + "palette", + ["vanilla", "pride", "white", "custom"], + "paletteModifier", +]; + +const customPaletteMenu = { + "palette[mode=default]": [ + ["TYPE_HEX", "0", 6, "customLevel0"], + ["TYPE_HEX", "1", 6, "customLevel1"], + ["TYPE_HEX", "2", 6, "customLevel2"], + ["TYPE_HEX", "3", 6, "customLevel3"], + ["TYPE_HEX", "4", 6, "customLevel4"], + ["TYPE_HEX", "5", 6, "customLevel5"], + ["TYPE_HEX", "6", 6, "customLevel6"], + ["TYPE_HEX", "7", 6, "customLevel7"], + ["TYPE_HEX", "8", 6, "customLevel8"], + ["TYPE_HEX", "9", 6, "customLevel9"], + ["TYPE_CUSTOM", "load vanilla", "LOAD_VANILLA"], + ["TYPE_CUSTOM", "load pride", "LOAD_PRIDE"], + ["TYPE_CUSTOM", "load white", "LOAD_WHITE"], + ["TYPE_CUSTOM", "load bugged", "LOAD_BUGGED"], + ["TYPE_NUMBER", "bug offset", 0, "buggedModifier"], + ], +}; + +const goToCustomPalette = ["TYPE_SUBMENU", "custom palette", customPaletteMenu]; +const crashModifier = [ + "TYPE_CHOICES", + "crash", + ["off", "show", "top", "crash"], + "crashModifier", +]; +const strictCrashFlag = ["TYPE_BOOL", "strict crash", "strictFlag"]; +const disablePause = ["TYPE_BOOL", "disable pause", "disablePauseFlag"]; +const debugFlag = ["TYPE_BOOL", "block tool", "debugFlag"]; +const palFlag = ["TYPE_BOOL", "pal mode", "palFlag"]; +const keyboardFlag = ["TYPE_BOOL", "keyboard", "keyboardFlag"]; +const qualFlag = ["TYPE_BOOL", "qual", "qualFlag"]; +const goofyFlag = ["TYPE_BOOL", "goofy foot", "goofyFlag"]; +const nextBoxStart = ["TYPE_CHOICES", "next piece", ['shown', 'hidden'], "nextBoxStart"]; +const clearScores = ["TYPE_CUSTOM", "clear scores", "CLEAR_SCOREBOARD"]; +const resetDefaults = ["TYPE_CUSTOM", "reset defaults", "RESET_DEFAULTS"]; + +const floorModifier = ["TYPE_NUMBER", "floor", 19, "floorModifier"]; +const crunchLeftModifier = [ + "TYPE_NUMBER", + "crunch left", + 4, + "crunchLeftModifier", +]; +const crunchRightModifier = [ + "TYPE_NUMBER", + "crunch right", + 4, + "crunchRightModifier", +]; +const invisibleFlag = ["TYPE_BOOL", "invisible", "invisibleOptionFlag"]; +const ghostPiece = ["TYPE_BOOL", "ghost", "ghostPieceFlag"]; +const hardDrop = ["TYPE_BOOL", "hardDrop", "hardDropFlag"]; + +const horizMirror = ["TYPE_BOOL", "mirror horiz", "mirrorHorizFlag"]; +const vertMirror = ["TYPE_BOOL", "mirror vert", "mirrorVertFlag"]; +const teppozFlag = ["TYPE_BOOL", "teppoz", "teppozFlag"]; +const sxtoklFlag = ["TYPE_BOOL", "sxtokl", "sxtoklFlag"]; +const palpepFlag = ["TYPE_BOOL", "palpep", "palpepFlag"]; +const splitSquareFlag = ["TYPE_BOOL", "split squares", "splitSquareFlag"]; +const startScore = ["TYPE_NUMBER", "score *100k", 16, "startScore"]; +const startLines = ["TYPE_NUMBER", "lines *10", 31, "startLines"]; +const transFlag = ["TYPE_BOOL", "trans", "transFlag"]; + +const presetModifier = [ + "TYPE_CHOICES", + "setup", + ["z", "t/s", "t", "i", "buco", "various", "ljspin", "ljdouble"], + "presetModifier", +]; +const typeBModifier = ["TYPE_NUMBER", "height", 9, "typeBModifier"]; +const typeBSeed = ["TYPE_HEX", "seed", 4, "b_seed_input"]; +const typeBSeedFlag = ["TYPE_BOOL", "seed enabled", "typeBSeedFlag"]; +const bTypeLines = ["TYPE_BCD", "lines", 2, "bTypeLines"]; +const checkerModifier = ["TYPE_NUMBER", "height", 9, "checkerModifier"]; +const quickTapLeftModifier = ["TYPE_NUMBER", "left", 20, "tapLeftModifier"]; +const quickTapRightModifier = ["TYPE_NUMBER", "right", 20, "tapRightModifier"]; +const quickTapLeftColumn = [ + "TYPE_CHOICES", + "left column", + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"], + "tapLeftColumn", +]; +const quickTapRightColumn = [ + "TYPE_CHOICES", + "right column", + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"], + "tapRightColumn", +]; +const practisePiece = [ + "TYPE_CHOICES", + "piece", + ["T", "J", "Z", "O", "S", "L", "I"], + "practisePiece", +]; +const marathonScoreFlag = [ + "TYPE_CHOICES", + "fixed score", + ["on", "off"], + "marathonScoreFlag", +]; +const marathonLevelModifier = [ + "TYPE_CHOICES", + "level up", + ["fixed", "normal", "zero"], + "marathonLevelModifier", +]; + +const tapqtyModifier = ["TYPE_NUMBER", "height", 16, "tapqtyModifier"]; +const noLineClearDelayFlag = [ + "TYPE_BOOL", + "no line clear", + "noLineClearDelayFlag", +]; +const garbageModifier = [ + "TYPE_CHOICES", + "mode", + ["tetris", "normal", "smart", "hard", "infinite"], + "garbageModifier", +]; +const droughtModifier = ["TYPE_NUMBER", "modifier", 20, "droughtModifier"]; +const lowStackRowModifier = [ + "TYPE_NUMBER", + "height", + 20, + "lowStackRowModifier", +]; + +const noWallChargeFlag = [ + "TYPE_CHOICES", + "wall charge", + ["on", "off"], + "noWallChargeFlag", +]; +const disableDasFlag = ["TYPE_CHOICES", "das", ["on", "off"], "disableDasFlag"]; +const anydasDas = ["TYPE_NUMBER", "delay", 31, "dasModifier"]; +const anydasArr = ["TYPE_NUMBER", "arrrr", 31, "arrModifier"]; +const anydasEntryDelay = [ + "TYPE_CHOICES", + "entry charge", + ["off", "hydrant", "kitaru"], + "entryChargeModifier", +]; +const trtFlag = ["TYPE_BOOL", "tetris rate", "trtFlag"]; +const dasMeterFlag = ["TYPE_BOOL", "das meter", "dasMeterFlag"]; +const gameTimerFlag = ["TYPE_BOOL", "game timer", "gameTimerFlag"]; + +const tapQtyMenu = { + "tap quantity[mode=tapqty]": [tapqtyModifier, noLineClearDelayFlag], +}; +const marathonMenu = { + "marathon[mode=marathon]": [marathonScoreFlag, marathonLevelModifier], +}; +const droughtMenu = { + "drought[mode=drought]": [droughtModifier], +}; +const checkerMenu = { + "checkerboard[mode=checkerboard]": [checkerModifier], +}; +const garbageMenu = { + "garbage[mode=garbage]": [garbageModifier], +}; +const lowstackMenu = { + "lowstack[mode=lowstack]": [lowStackRowModifier], +}; + +const bMenu = { + "b-type[mode=typeb]": [typeBModifier, typeBSeed, typeBSeedFlag, bTypeLines], +}; +const setupsMenu = { + "setups[mode=presets]": [presetModifier], +}; +const quickTapMenu = { + "(quick)tap[mode=tap]": [ + quickTapLeftModifier, + quickTapRightModifier, + quickTapLeftColumn, + quickTapRightColumn, + practisePiece, + debugFlag, + ], +}; +const mainMenu = { + "play tetris[mode=tetris]": [ + ["TYPE_GAMEMODE", "t-spins", "MODE_TSPINS"], + ["TYPE_GAMEMODE", "stacking", "MODE_STACKING"], + ["TYPE_SUBMENU", "setups", setupsMenu], + ["TYPE_SUBMENU", "b-type", bMenu], + ["TYPE_SUBMENU", "(quick)tap", quickTapMenu], + ["TYPE_SUBMENU", "marathon", marathonMenu], + ["TYPE_SUBMENU", "tap quantity", tapQtyMenu], + ["TYPE_SUBMENU", "checkerboard", checkerMenu], + ["TYPE_SUBMENU", "garbage", garbageMenu], + ["TYPE_SUBMENU", "drought", droughtMenu], + ["TYPE_SUBMENU", "lowstack", lowstackMenu], + ["TYPE_GAMEMODE", "kill*2", "MODE_KILLX2"], + ["TYPE_GAMEMODE", "tap/roll speed", "MODE_SPEED_TEST"], + ], + "tournament[mode=default]": [ + seedInput, + seedFlag, + linecapWhen, + linecapHow, + linecapLevel, + linecapLines, + dasOnly, + vitsScoreFlag, + ], + "general[mode=default]": [ + crashModifier, + strictCrashFlag, + nextBoxStart, + disablePause, + debugFlag, + goofyFlag, + qualFlag, + palFlag, + keyboardFlag, + ["TYPE_GAMEMODE", "calibrate", "MODE_CALIBRATE"], + clearScores, + resetDefaults, + ], + + "modify game[mode=default]": [ + floorModifier, + crunchLeftModifier, + crunchRightModifier, + invisibleFlag, + ghostPiece, + hardDrop, + horizMirror, + vertMirror, + teppozFlag, + sxtoklFlag, + palpepFlag, + splitSquareFlag, + startScore, + startLines, + transFlag, + ], + + "display[mode=default]": [ + scoringModifier, + modernLinesFlag, + hzFlag, + inputDisplayFlag, + darkMode, + disableFlash, + paceModifier, + trtFlag, + dasMeterFlag, + gameTimerFlag, + secretGrading, + paletteSelection, + goToCustomPalette, + ], + + "handling[mode=default]": [ + anydasDas, + anydasArr, + anydasEntryDelay, + noWallChargeFlag, + disableDasFlag, + ], +}; + +const extraSpriteStrings = [ + "pause", + "block", + "clear?", + "sure?!", + "confetti", + "wait", +]; + +module.exports = { mainMenu, extraSpriteStrings }; diff --git a/src/modes/calibrate.asm b/src/modes/calibrate.asm new file mode 100644 index 00000000..c9bb5cbb --- /dev/null +++ b/src/modes/calibrate.asm @@ -0,0 +1,222 @@ +gameMode_calibrate: +; stays in loop until ABSS + +@fillModifier = anydasFlag +; 0-3, increases on up/down +; rotates through a blank board or filled with each of the three minos +; refreshes pattern if not in fill mode + +; levelNumber +; 0-9, increase/decrease with left/right + +@fillFlag = tetriminoY +; 0-1, toggled by B +; 0 = show random pattern +; 1 = fill board with @fillModifier + +@suspendRefresh = fallTimer +; 0-1, toggled by start +; 0 = refresh pattern every 128 frames +; 1 = stay on current pattern + +@nextBoxPiece = tetriminoX +; 0-6, increases on select and wraps around at 6 +; corresponding stats counter increases on each increment +; hold A to rotate once per frame + + jsr gameModeState_initGameBackground + jsr gameModeState_initGameState + ldx nextPiece + stx currentPiece + lda tetriminoTypeFromOrientation,x + sta @nextBoxPiece + +@refreshPattern: + lda #0 + sta vramRow + lda @fillFlag + bne @tilefill + ldy #15 +@fill: + ldx #b_seed + jsr generateNextPseudorandomNumber5x + lda oneThirdPRNG + clc + adc #$7B + sta mathRAM,y + dey + bpl @fill + + lda rng_seed+1 + and #7 + tax + lda #$EF + sta mathRAM,x + sta mathRAM+8,x + + ldy #200 + lda rng_seed + and #15 + tax +@loop: + lda mathRAM,x + sta playfield-1,y + dex + bpl :+ + ldx #15 +: + dey + bne @loop + jmp @waitLoop + +@tilefill: + ldx #200 + lda @fillModifier + beq @empty + lda #$7A + clc + adc @fillModifier + bne @tile +@empty: + lda #EMPTY_TILE +@tile: + sta playfield-1,x + dex + bne @tile + jmp @waitLoop + +@checkInputs: + +; reset sequence + lda heldButtons_player1 + cmp #BUTTON_A+BUTTON_B+BUTTON_SELECT+BUTTON_START + bne @noReset + lda #GAMEMODE_GAMETYPEMENU + sta gameMode + rts +@noReset: + +; B to toggle @fillFlag + lda newlyPressedButtons_player1 + and #BUTTON_B + beq @noToggle + lda @fillFlag + eor #1 + sta @fillFlag + jmp @refreshPattern +@noToggle: + +; up/down to rotate @fillModifier + lda newlyPressedButtons_player1 + and #BUTTON_UP|BUTTON_DOWN + beq @upNotPressed + inc @fillModifier + lda @fillModifier + and #3 + sta @fillModifier + jmp @refreshPattern +@upNotPressed: + +; start to suspend pattern refresh + lda newlyPressedButtons_player1 + and #BUTTON_START + beq @startNotPressed + lda @suspendRefresh + eor #1 + sta @suspendRefresh + bne @waitLoop + jmp @refreshPattern +@startNotPressed: + +; select to rotate piece +; A+select to rapdily rotate + lda newlyPressedButtons_player1 + and #BUTTON_SELECT + bne @rotatePiece + lda heldButtons_player1 + cmp #BUTTON_A+BUTTON_SELECT + bne @noPieceRotate + +@rotatePiece: + lda nextPiece + sta currentPiece + jsr incrementPieceStat + inc @nextBoxPiece + lda @nextBoxPiece + cmp #7 + bne @noRollover + lda #0 + sta @nextBoxPiece +@noRollover: + tax + lda spawnTable,x + sta nextPiece + jmp @waitLoop + +; left/right to decrease/increase levelNumber +@noPieceRotate: + lda newlyPressedButtons_player1 + and #3 + beq @checkFrameCounter + lsr + bcs @rightPressed + +; left pressed + dec levelNumber + bpl @renderLevel + lda #9 + sta levelNumber + bne @renderLevel + +@rightPressed: + inc levelNumber + lda levelNumber + cmp #$0A + bcc @renderLevel + lda #0 + sta levelNumber + +@renderLevel: + lda levelNumber + +; optional 7 digit + ldy scoringModifier + cpy #2 + beq @sevenDigit + lda #0 +@sevenDigit: + sta bcd32+3 + +; fill score & lines, render & exit + lda levelNumber + sta lines+1 + asl + asl + asl + asl + ora levelNumber + sta lines + sta bcd32 + sta bcd32+1 + sta bcd32+2 + jsr presetScoreFromBCD + lda #RENDER_LINES|RENDER_LEVEL|RENDER_SCORE + sta renderFlags + +@waitLoop: + jsr stageSpriteForNextPiece + jsr updateAudioWaitForNmiAndResetOamStaging + jmp @checkInputs + +; shuffle every 128 frames +@checkFrameCounter: + lda @suspendRefresh + bne @waitLoop + lda frameCounter + and #$7F + beq @refresh + jmp @waitLoop +@refresh: + jmp @refreshPattern + +.out .sprintf("Calibrate code: %d", *-gameMode_calibrate) diff --git a/src/modes/crash.asm b/src/modes/crash.asm index 19899240..5f767a82 100644 --- a/src/modes/crash.asm +++ b/src/modes/crash.asm @@ -1,8 +1,8 @@ ; Hydrant's crash theory sheet https://docs.google.com/spreadsheets/d/1zAQIo_mnkk0c9e4-hpeDvVxrl9r_HvLSx8V4h4ttmrs/edit#gid=1013692687 testCrash: - lda #$00 - sta lagState ;clearing lag state between calculations + lda #$00 + sta lagState ;clearing lag state between calculations lda #$1C ; setting all cycles which always happen. for optimizing, this can be removed if all compared numbers are reduced by $6F1C. sta cycleCount lda #$6F @@ -410,11 +410,7 @@ testCrash: sta allegroIndex lda lagState beq @noLag ;if lag should happen, wait a frame here so that sprite staging doesn't happen. - lda #$00 - sta verticalBlankingInterval -@checkForNmi: - lda verticalBlankingInterval ;busyloop - beq @checkForNmi + jsr waitForNmi @noLag: rts @crashGraphics: lda #$00 @@ -458,16 +454,13 @@ confettiHandler: beq @endConfetti @drawConfetti: sta spriteYOffset ;either frameCounter or 80 loaded to A depending on confetti type - lda #$A8 ;center of playfield + lda #$68 ; center of playfield sta spriteXOffset - lda #$19 ;ID for "confetti" text - sta spriteIndexInOamContentLookup - jsr stringSpriteAlignRight ;draw to screen + ldx #>STR_CONFETTI + ldy #secretGrade + sta byteSpriteAddr+1 + lda #0 + sta byteSpriteTile + lda #1 + sta byteSpriteLen + jmp byteSprite diff --git a/src/nametables.asm b/src/nametables.asm index 70dd5f9d..d4893897 100644 --- a/src/nametables.asm +++ b/src/nametables.asm @@ -1,7 +1,32 @@ +.enum + RLE_NT_ROCKET + RLE_NT_GAME + RLE_NT_GAME_MENU + RLE_NT_LEGAL + RLE_NT_LEVEL_MENU + RLE_NT_HIGH_SCORE +.endenum + +rleNametables: + .addr rocket_nametable + .addr game_nametable + .addr game_type_menu_nametable + .addr legal_nametable + .addr level_menu_nametable + .addr enter_high_score_nametable + +loadRleNametableXToTmp: + txa + asl + tax + lda rleNametables,x + sta tmp1 + lda rleNametables+1,x + sta tmp2 + rts + game_type_menu_nametable: ; RLE .incbin "nametables/game_type_menu_nametable_practise.bin" -game_type_menu_nametable_extra: ; RLE - .incbin "nametables/game_type_menu_nametable_extra.bin" level_menu_nametable: ; RLE .incbin "nametables/level_menu_nametable_practise.bin" game_nametable: ; RLE @@ -12,25 +37,25 @@ rocket_nametable: ; RLE .incbin "nametables/rocket_nametable.bin" legal_nametable: ; RLE .incbin "nametables/legal_nametable.bin" -title_nametable_patch: ; stripe - .byte $21, $69, $5, $1D, $12, $1D, $15, $E - .byte $FF -rocket_nametable_patch: ; stripe - .byte $20, $83, 5, $19, $1B, $E, $1c, $1c - .byte $20, $A3, 5, $1c, $1d, $a, $1b, $1d - .byte $FF +titleNametablePatch: ; stripe + .byte $21, $69, $4, $1D, $12, $1D, $15, $E + .byte $0 +rocketNametablePatch: ; stripe + .byte $20, $83, 4, $19, $1B, $E, $1c, $1c + .byte $20, $A3, 4, $1c, $1d, $a, $1b, $1d + .byte $0 -speedtest_nametable_patch: +speedtestNametablePatch: ; tiles - .byte $21, $A3, $6, 0, 0, $ED, 0, 0, $EC - .byte $22, $23, $3, 'T', 'A', 'P' - .byte $22, $A3, $3, 'D', 'I', 'R' - .byte $22, $28, $1, 0 + .byte $21, $A3, $5, 0, 0, $ED, 0, 0, $EC + .byte $22, $23, $2, 'T', 'A', 'P' + .byte $22, $A3, $2, 'D', 'I', 'R' + .byte $22, $28, $0, 0 ; attrs - .byte $23, $e2, $1, 0 - .byte $23, $ea, $1, 0 - .byte $23, $d8, $43, $55 - .byte $FF + .byte $23, $e2, $0, 0 + .byte $23, $ea, $0, 0 + .byte $23, $d8, $2, $55, $55, $55 + .byte $00 .include "nametables/rle.asm" diff --git a/src/nametables/game_type_menu.js b/src/nametables/game_type_menu.js index a12cb1a1..d691c5eb 100644 --- a/src/nametables/game_type_menu.js +++ b/src/nametables/game_type_menu.js @@ -5,7 +5,9 @@ const { drawRect, drawAttrs, flatLookup, -} = require('./nametables'); +} = require("./nametables"); + +const { spawnSync } = require("child_process"); const lookup = flatLookup(` 0123456789ABCDEF @@ -13,7 +15,7 @@ GHIJKLMNOPQRSTUV WXYZ-,˙>######## ########qweadzxc ###############/ -##!##~######[]() +##!##~###tyu[]() #########»#####. ################ ################ @@ -27,58 +29,27 @@ WXYZ-,˙>######## `); const buffer = blankNT(); -const extra = [...buffer]; -drawTiles(buffer, lookup, ` -#a d# -#a d# +drawTiles( + buffer, + lookup, + ` #a d# +#a uu d# +#a tt uu ttt d# +#a tt t d# +#a t y d# +#a yy t uuu y d# +#a yy t u yy d# +#a t d# #a d# #a d# #a d# #a d# #a d# #a d# -#a TETRIS d# -#a T-SPINS d# -#a SEED d# -#a STACKING d# -#a PACE d# -#a SETUPS d# -#a B-TYPE d# -#a FLOOR d# -#a CRUNCH d# -#a (QUICK)TAP d# -#a TRANSITION d# -#a MARATHON d# -#a TAP QUANTITY d# -#a CHECKERBOARD d# -#a GARBAGE d# -#a DROUGHT d# -#a DAS DELAY d# -#a LOW STACK d# -#a KILLSCREEN »2 d# -#a INVISIBLE d# -#a HARD DROP d# -`);drawTiles(extra, lookup, ` -#a TAP/ROLL SPEED d# -#a SCORING d# -#a CRASH d# -#a STRICT CRASH d# -#a HZ DISPLAY d# -#a INPUT DISPLAY d# -#a DISABLE FLASH d# -#a DISABLE PAUSE d# -#a DARK MODE d# -#a GOOFY FOOT d# -#a BLOCK TOOL d# -#a LINECAP d# -#a DAS ONLY d# -#a QUAL MODE d# -#a PAL MODE d# #a d# #a d# -#a V6 d# #a d# #a d# #a d# @@ -93,11 +64,15 @@ drawTiles(buffer, lookup, ` #a d# #a d# #a d# -`); +`, +); -if (process.env['GYM_FLAGS']?.match(/-D KEYBOARD=1/)) { - drawTiles(extra, lookup, "KEYBOARD", 32 * 15 + 6); - } +spawnSync("git", ["describe", "--tags"]) + .stdout.toString() + .match(/./g) + .forEach( + (c, i) => (buffer[32 * 28 + 6 + i] = lookup.indexOf(c.toUpperCase())), + ); const background = ` ɢ##############################ɳ @@ -133,61 +108,36 @@ const background = ` `; drawTiles(buffer, lookup, background); -drawTiles(extra, lookup, background); -drawRect(buffer, 8, 2, 10, 5, 0xB0); // draw logo +drawRect(buffer, 8, 2, 10, 5, 0xb0); // draw logo -const urlX = 3; -const urlY = 17; -drawRect(extra, urlX, urlY, 12, 1, 0x74); -drawRect(extra, urlX+12, urlY, 12, 1, 0x84); +// const urlX = 3; +// const urlY = 27; -drawAttrs(buffer, [` - 2222222222222222 - 2222211111122222 - 2222211111122222 - 2222211111122222 - 2222222222222222 - 2222222222222222 - 2222222222222222 - 2222222222222222 -`,` - 2222222222222222 - 2222222222222222 - 2222222222222222 - 2222222222222222 - 2222222222222222 - 2222222222222222 - 2222222222222222 - 2222222222222222 -`]); +// drawRect(buffer, urlX, urlY, 12, 1, 0x74); +// drawRect(buffer, urlX+12, urlY, 12, 1, 0x84); -drawAttrs(extra, [` - 2222222222222222 - 2222222222222222 - 2222222222222222 +drawAttrs(buffer, [ + ` + 2000000000000002 + 2000011111100002 + 2000011111100002 + 2000011111100002 2222222222222222 2222222222222222 2222222222222222 2222222222222222 - 2222222222222222 -`, ` - 2333333333333332 +`, + ` 2222222222222222 2222222222222222 2222222222222222 2222222222222222 2222222222222222 + 2333333333333332 2222222222222222 2222222222222222 -`]); +`, +]); -writeRLE( - __dirname + '/game_type_menu_nametable_practise.bin', - buffer, -); - -writeRLE( - __dirname + '/game_type_menu_nametable_extra.bin', - extra, -); +writeRLE(__dirname + "/game_type_menu_nametable_practise.bin", buffer); diff --git a/src/nametables/rle.asm b/src/nametables/rle.asm index cfac3151..8557218a 100644 --- a/src/nametables/rle.asm +++ b/src/nametables/rle.asm @@ -36,7 +36,7 @@ copyRleNametableToPpu: lda #$20 sta addrOff copyRleNametableToPpuOffset: - jsr copyAddrAtReturnAddressToTmp_incrReturnAddrBy2 + jsr loadRleNametableXToTmp ldx PPUSTATUS lda addrOff sta PPUADDR diff --git a/src/nmi/nmi.asm b/src/nmi/nmi.asm index b008898e..60b692f8 100644 --- a/src/nmi/nmi.asm +++ b/src/nmi/nmi.asm @@ -1,43 +1,59 @@ nmi: pha + lda renderMode + beq restoreA txa pha tya pha - lda #$00 - sta oamStagingLength jsr render + lda ppuScrollX + sta PPUSCROLL + lda ppuScrollY + sta PPUSCROLL lda currentPpuCtrl sta PPUCTRL - dec sleepCounter - lda sleepCounter - cmp #$FF - bne @jumpOverIncrement - inc sleepCounter -@jumpOverIncrement: - jsr copyOamStagingToOam + lda #$00 + sta OAMADDR + lda #$02 + sta OAMDMA renderComplete: - lda frameCounter - clc - adc #$01 - sta frameCounter - lda #$00 - adc frameCounter+1 - sta frameCounter+1 + lda sleepCounter + beq @noSleep + dec sleepCounter +@noSleep: + + inc frameCounter + bne @noCarry + inc frameCounter+1 +@noCarry: + ldx #rng_seed jsr generateNextPseudorandomNumber - jsr copyCurrentScrollAndCtrlToPPU + ldx #b_seed + jsr generateNextPseudorandomNumber + jsr pollControllerButtons + + ; advance game timer + lda gameTimerStop + bne nmiFinish + inc gameTimer+1 + bne nmiFinish + inc gameTimer +nmiFinish: lda #$00 + sta oamStagingLength sta lagState ; clear flag after lag frame achieved lda #$01 sta verticalBlankingInterval - pla - tay tsx - lda stack+4,x + lda stack+5,x sta nmiReturnAddr pla + tay + pla tax +restoreA: pla irq: rti diff --git a/src/nmi/pollcontroller.asm b/src/nmi/pollcontroller.asm index 4f89b1ec..503f34af 100644 --- a/src/nmi/pollcontroller.asm +++ b/src/nmi/pollcontroller.asm @@ -66,7 +66,6 @@ pollController: diffOldAndNewButtons: ldx #$01 -.if KEYBOARD = 1 ; clear controller input when keyboard is active ; disable keyboard when reset sequence is pressed lda keyboardFlag @@ -80,7 +79,6 @@ diffOldAndNewButtons: plp bne @ret sta keyboardFlag -.endif @diffForPlayer: lda newlyPressedButtons_player1,x tay diff --git a/src/nmi/render.asm b/src/nmi/render.asm index 1858e84d..91903374 100644 --- a/src/nmi/render.asm +++ b/src/nmi/render.asm @@ -1,28 +1,40 @@ +.enum +RENDER_DISABLE +RENDER_IDLE +RENDER_CONGRATS +RENDER_PLAY +RENDER_PAUSE +RENDER_ROCKET +RENDER_SPEED_TEST +RENDER_LEVEL_MENU +RENDER_PLAYFIELD +RENDER_TOPROW +RENDER_QUEUE +.endenum + render: branchTo renderMode, \ - render_mode_static, \ - render_mode_scroll, \ + render_mode_disable, \ + render_mode_idle, \ render_mode_congratulations_screen, \ render_mode_play_and_demo, \ render_mode_pause, \ render_mode_rocket, \ render_mode_speed_test, \ render_mode_level_menu, \ - render_mode_linecap_menu + render_mode_dump_playfield, \ + render_mode_top_row, \ + render_mode_queue .include "render_mode_level_menu.asm" ; no rts / jmp -render_mode_static: - lda currentPpuCtrl - and #$FC - sta currentPpuCtrl - jsr resetScroll +render_mode_idle: +render_mode_disable: rts .include "render_mode_linecap.asm" .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/nmi/render_hz.asm b/src/nmi/render_hz.asm index da0db410..8fc6544a 100644 --- a/src/nmi/render_hz.asm +++ b/src/nmi/render_hz.asm @@ -23,7 +23,6 @@ renderHz: renderHzSpeedTest: ; palette - lda #$3F sta PPUADDR lda #$07 diff --git a/src/nmi/render_mode_linecap.asm b/src/nmi/render_mode_linecap.asm index 8fe02656..8d509a47 100644 --- a/src/nmi/render_mode_linecap.asm +++ b/src/nmi/render_mode_linecap.asm @@ -12,18 +12,21 @@ render_mode_linecap_menu: jsr render_linecap_level_lines @static: - jmp render_mode_static + rts render_linecap_level_lines: lda linecapWhen - bne @linecapLines + cmp #LINECAP_LINES + beq @linecapLines + cmp #LINECAP_LEVEL + bne @ret lda linecapLevel - jsr renderByteBCD - jmp render_mode_static + jmp renderByteBCD @linecapLines: - lda linecapLines+1 - sta PPUDATA lda linecapLines + sta PPUDATA + lda linecapLines+1 jsr twoDigsToPPU +@ret: rts diff --git a/src/nmi/render_mode_play_and_demo.asm b/src/nmi/render_mode_play_and_demo.asm index e2bb7670..0d7fe696 100644 --- a/src/nmi/render_mode_play_and_demo.asm +++ b/src/nmi/render_mode_play_and_demo.asm @@ -13,7 +13,16 @@ render_mode_play_and_demo: jsr render_playfield @renderLines: - lda scoringModifier + lda trtScratch+5 + beq LFC0C + ldx #$23 + stx PPUADDR + ldx #$38 + stx PPUADDR + jsr twoDigsToPPU +LFC0C: + + lda modernLinesFlag bne @modernLines lda renderFlags @@ -262,6 +271,15 @@ updateLineClearingAnimation: ldx generalCounter3 lda completedRow,x beq @nextRow + lda mirrorVertFlag + beq @notVertMirror + lda #$13 + sec + sbc completedRow,x + bcs @adjustRow ; completedRow should never be more than $13 +@notVertMirror: + lda completedRow,x +@adjustRow: asl a tay lda vramPlayfieldRows,y @@ -312,20 +330,40 @@ updatePaletteForLevel: ldx #$00 @loadLevelNumber: lda levelNumber,x -@mod10: cmp #$0A + php ; keep track of glitched color range +@mod10: cmp #GameColors::bugged bmi @copyPalettes ; bcc fixes the colour bug sec - sbc #$0A + sbc #GameColors::bugged jmp @mod10 @copyPalettes: and #$3F tax + plp + bmi @checkPal ; skip custom palette when in glitched colors + cpx #GameColors::bugged + bcs @checkPal + ldy paletteModifier + beq @checkPal + dey + beq @pride + dey + beq @white + jmp @customPalette +@white: + ; all white + ldx #GameColors::white + bne @renderPalettes +@pride: + adc #GameColors::pride + tax +@checkPal: lda palFlag beq @renderPalettes cpx #$35 ; Level 181 & 245 and'd with $3F (level 53 & 117 are properly mod10'd) bne @renderPalettes - ldx #$40 + ldx #GameColors::pal181 @renderPalettes: lda #$3F sta PPUADDR @@ -356,6 +394,46 @@ updatePaletteForLevel: @done: rts +@customPalette: + ldy multBy3,x + lda #$3F + sta PPUADDR + lda #$09 + sta PPUADDR + lda customLevel0,y + sta PPUDATA + lda customLevel0+1,y + sta PPUDATA + lda customLevel0+2,y + sta PPUDATA + ldx darkModifier + cpx #2 ; neon + bne @stillNotNeon + sta PPUDATA +@stillNotNeon: + lda #$3F + sta PPUADDR + lda #$19 + sta PPUADDR + lda customLevel0,y + sta PPUDATA + lda customLevel0+1,y + sta PPUDATA + lda customLevel0+2,y + sta PPUDATA + rts + +multBy3: + .byte 0,3,6,9,12,15,18,21,24,27 + +.struct GameColors + normal .byte 10 + bugged .byte 54 + pal181 .byte + pride .byte 10 + white .byte 1 +.endstruct + ; 3 bytes per level in separate tables colorTable0: .byte $30,$30,$30,$30 @@ -375,6 +453,12 @@ colorTable0: .byte $06,$4C,$BD,$19 .byte $00,$01,$03,$05 .byte $21 ; level 181/245 pal (different from NTSC) +; pride colors + .byte $30,$30,$25,$30 + .byte $30,$30,$30,$30 + .byte $30,$30 +; all white + .byte $30 colorTable1: .byte $21,$29,$24,$2A @@ -394,6 +478,12 @@ colorTable1: .byte $38,$2A,$4E,$60 .byte $00,$01,$04,$05 .byte $2b ; level 181/245 pal (same as NTSC) +; pride colors + .byte $00,$00,$11,$00 + .byte $28,$21,$27,$00 + .byte $25,$27 +; all white + .byte $30 colorTable2: .byte $12,$1A,$14,$12 @@ -413,6 +503,12 @@ colorTable2: .byte $E9,$99,$99,$00 .byte $01,$02,$04,$05 .byte $25 ; level 181/245 pal (same as NTSC) +; pride colors + .byte $21,$1a,$14,$25 + .byte $14,$2b,$11,$14 + .byte $21,$15 +; all white + .byte $30 incrementPieceStat: tax @@ -445,3 +541,67 @@ L9996: lda generalCounter ora #RENDER_STATS sta renderFlags rts +stageFullPlayfield: + lda invisibleFlag + beq @notInviz + jmp @resetVramRow +@notInviz: + ldy #18 +@loop: + ldx multBy10Table,y +.repeat 10,i + lda playfield+10+i,x + sta $100+(i*19),y +.endrepeat + dey + bpl @loop + lda #RENDER_PLAYFIELD + sta renderMode +@resetVramRow: + lda #$20 + sta vramRow + rts + +render_mode_dump_playfield: +; handles 95% of the playfield + lda #$9C + sta PPUCTRL + tsx + txa + ldx #$FF + txs + tax + .repeat 10,i + lda #$20 + sta PPUADDR + lda #$EC+i + sta PPUADDR + .repeat 19 + pla + sta PPUDATA + .endrepeat + .endrepeat + txs + lda #RENDER_TOPROW + sta renderMode + rts + +render_mode_top_row: +; follows render_mode_dump_playfield to handle top row + lda #$20 + sta PPUADDR + lda #$CC + sta PPUADDR + ldx #0 + ldy #9 +@loop: + lda playfield,x + sta PPUDATA + inx + dey + bpl @loop + lda #RENDER_PLAY + sta renderMode + jmp render_mode_play_and_demo + +.out .sprintf("instant harddrop rendering: %d", *-stageFullPlayfield) diff --git a/src/nmi/render_mode_queue.asm b/src/nmi/render_mode_queue.asm new file mode 100644 index 00000000..2a3d4dbc --- /dev/null +++ b/src/nmi/render_mode_queue.asm @@ -0,0 +1,126 @@ +render_mode_queue: + lda #>dump01Tiles + sta tmp2 + tsx + txa + tay + ldx #$FF + txs +checkQueueLength: + lda renderQueueLength + bne @stripe + jmp restoreStackPointer +@stripe: + pla + sta PPUADDR + pla + sta PPUADDR + pla ; 0 = 1 tile, max 32 tiles + tax + lda queueJumpTable,x + sta tmp1 + jmp (tmp1) +.repeat 32,i +.ident(.sprintf("dump%02dTiles", 32-i)): + pla + sta PPUDATA +.endrepeat + dec renderQueueLength + beq restoreStackPointer + jmp checkQueueLength +restoreStackPointer: + tya + tax + txs +resetRenderQueue: + lda #0 + sta renderQueueLength + sta renderQueuePointer + rts + +queueJumpTable: +.repeat 32,i + .byte <.ident(.sprintf("dump%02dTiles", i+1)) +.endrepeat + +.out .sprintf("render queue dump: %d", *-render_mode_queue) +.assert >dump01Tiles=>dump32Tiles,error,"render queue needs to exist in one page" + + +.struct GameRender + WithPlayfield .struct + WithoutPlayfield .struct + paletteAddr .word + paletteLen .byte + paletteTiles .byte 32 + + scoreAddr .word + scoreLen .byte + scoreTiles .byte 7 + + linesAddr .word + linesLen .byte + linesTiles .byte 4 + + levelAddr .word + levelLen .byte + levelTiles .byte 3 + .endstruct + pfield0Addr .word + pfield0Len .byte + pfield0Tiles .byte 10 + pfield1Addr .word + pfield1Len .byte + pfield1Tiles .byte 10 + pfield2Addr .word + pfield2Len .byte + pfield2Tiles .byte 10 + pfield3Addr .word + pfield3Len .byte + pfield3Tiles .byte 10 + .endstruct +.endstruct + +PALETTE_ADDR = $3f00 + +; placeholder values +SCORE_ADDR = $2000 +LINES_ADDR = $2000 +LEVEL_ADDR = $2000 + + +initializeGameRender: + lda #>PALETTE_ADDR + sta stack+GameRender::paletteAddr + lda #SCORE_ADDR + sta stack+GameRender::scoreAddr + lda #LINES_ADDR + sta stack+GameRender::linesAddr + lda #LEVEL_ADDR + sta stack+GameRender::levelAddr + lda #rocketNametablePatch + jsr copyPatchAtXYToQueue + jsr render_mode_queue + @stage2: @rocketEnd: jsr resetScroll diff --git a/src/nmi/render_mode_scroll.asm b/src/nmi/render_mode_scroll.asm deleted file mode 100644 index 13de6fc7..00000000 --- a/src/nmi/render_mode_scroll.asm +++ /dev/null @@ -1,43 +0,0 @@ -render_mode_scroll: - ; handle scroll - lda currentPpuCtrl - and #$FC - sta currentPpuCtrl - lda #0 - sta ppuScrollX - - jsr calc_menuScrollY - cmp menuScrollY - beq @endscroll - ; not equal - cmp menuScrollY - bcc @lessThan - - inc menuScrollY - - jmp @endscroll -@lessThan: - dec menuScrollY -@endscroll: - - lda menuScrollY - cmp #MENU_MAX_Y_SCROLL - bcc @uncapped - lda #MENU_MAX_Y_SCROLL - sta menuScrollY -@uncapped: - - sta ppuScrollY - rts - -calc_menuScrollY: - lda practiseType - cmp #MENU_TOP_MARGIN_SCROLL - bcs @underflow - lda #MENU_TOP_MARGIN_SCROLL+1 -@underflow: - sbc #MENU_TOP_MARGIN_SCROLL - asl - asl - asl - rts diff --git a/src/nmi/render_mode_speed_test.asm b/src/nmi/render_mode_speed_test.asm index 72e97c12..a1527588 100644 --- a/src/nmi/render_mode_speed_test.asm +++ b/src/nmi/render_mode_speed_test.asm @@ -6,8 +6,4 @@ render_mode_speed_test: lda #0 sta renderFlags @noUpdate: - lda #$B0 - sta ppuScrollX - lda #$0 - sta ppuScrollY rts diff --git a/src/nmi/render_score.asm b/src/nmi/render_score.asm index 52b80755..4e9076e3 100644 --- a/src/nmi/render_score.asm +++ b/src/nmi/render_score.asm @@ -232,4 +232,4 @@ renderLettersHighByte: rts linesDash: - .byte $15, $12, $17, $E, $1C, $24 + .byte "LINES", $24 diff --git a/src/nmi/render_util.asm b/src/nmi/render_util.asm index 8467ae85..1152da06 100644 --- a/src/nmi/render_util.asm +++ b/src/nmi/render_util.asm @@ -66,6 +66,10 @@ vramPlayfieldRows: .word $22CC,$22EC,$230C,$232C copyLowStackRowToVram: + ldy #0 + jsr crunchAdjustYSetX + txa + pha sec lda #19 sbc lowStackRowModifier @@ -73,9 +77,12 @@ copyLowStackRowToVram: tax lda vramPlayfieldRows+1,x sta PPUADDR - lda vramPlayfieldRows,x + tya + clc + adc vramPlayfieldRows,x sta PPUADDR - ldx #$0A + pla + tax lda #LOW_STACK_LINE @drawLine: sta PPUDATA @@ -88,7 +95,19 @@ copyPlayfieldRowToVRAM: cpx #$15 bpl @ret lda multBy10Table,x + ldy mirrorHorizFlag + beq @notHorizMirror + clc + adc #$9 +@notHorizMirror: tay + lda mirrorVertFlag + beq @notVertMirror + lda #$13 + sec + sbc vramRow + tax +@notVertMirror: txa asl a tax @@ -96,15 +115,16 @@ copyPlayfieldRowToVRAM: lda vramPlayfieldRows,x sta PPUADDR dex - lda vramPlayfieldRows,x sta PPUADDR @copyRow: ldx #$0A lda invisibleFlag bne @copyRowInvisible + lda mirrorHorizFlag + bne @copyRowMirrorHoriz @copyByte: - lda (playfieldAddr),y + lda playfield,y sta PPUDATA iny dex @@ -125,3 +145,11 @@ copyPlayfieldRowToVRAM: dex bne @copyByteInvisible jmp @rowCopied + +@copyRowMirrorHoriz: + lda playfield,y + sta PPUDATA + dey + dex + bne @copyRowMirrorHoriz + jmp @rowCopied diff --git a/src/palettes.asm b/src/palettes.asm index 3fbb3e0f..6d5cbf02 100644 --- a/src/palettes.asm +++ b/src/palettes.asm @@ -3,9 +3,9 @@ ; palette data ; $FF -game_palette: +gamePalette: .byte $3F,$00 - .byte $20 + .byte $1F .byte $0F,$30,$12,$16 ; bg .byte $0F,$20,$12,$00 .byte $0F,$2C,$16,$29 @@ -13,30 +13,30 @@ game_palette: .byte $0F,$16,$2A,$22 ; sprite .byte $0F,$10,$16,$2D .byte $0F,$2C,$16,$29 - .byte $0F,$3C,$00,$30 - .byte $FF -title_palette: + .byte $0F,$2A,$27,$16 + .byte $00 +titlePalette: .byte $3F,$00 - .byte $14 - .byte $0F,$3C,$38,$00 ; bg + .byte $13 + .byte $0F,$0F,$0F,$0F ; bg .byte $0F,$17,$27,$37 .byte $0F,$30,MENU_HIGHLIGHT_COLOR,$00 .byte $0F,$22,$2A,$28 - .byte $0F,$30,$29,$27 ; sprite - .byte $FF -menu_palette: + .byte $0F,$27,$29,$27 ; sprite + .byte $00 +menuPalette: .byte $3F,$00 - .byte $16 + .byte $15 .byte $0F,$30,$38,$26 ; bg .byte $0F,$17,$27,$37 .byte $0F,$30,MENU_HIGHLIGHT_COLOR,$00 .byte $0F,$16,$2A,$28 .byte $0F,$16,$26,$27 ; sprite .byte $0F,$2A - .byte $FF -rocket_palette: + .byte $00 +rocketPalette: .byte $3F,$11 - .byte $07 + .byte $06 .if INES_MAPPER = 0 ; sprite .byte $2D,$30,$27 ; Ufo colors .else @@ -44,16 +44,18 @@ rocket_palette: .endif .byte $0F,$37,$18,$38 .byte $3F,$00 - .byte $08 + .byte $07 .byte $0F,$3C,$38,$00 ; bg .byte $0F,$20,$12,$15 - .byte $FF -wait_palette: - .byte $3F,$11 - .byte $01 - .byte $30 ; sprite + .byte $0 + +waitPalettePatch: + .byte $3F,$10 + .byte $05 + .byte $0F,$30,$12,$13 ; sprite + .byte $0F,$16 .byte $3F,$00 - .byte $08 + .byte $07 .byte $0F,$30,$38,$26 ; bg .byte $0F,$17,$27,$37 - .byte $FF + .byte $00 diff --git a/src/playstate/active.asm b/src/playstate/active.asm index 2299ded3..c8c0fc54 100644 --- a/src/playstate/active.asm +++ b/src/playstate/active.asm @@ -1,7 +1,6 @@ playState_playerControlsActiveTetrimino: - lda practiseType - cmp #MODE_HARDDROP - bne @notHard + lda hardDropFlag + beq @notHard jsr harddrop_tetrimino lda playState cmp #8 @@ -20,7 +19,17 @@ playState_playerControlsActiveTetrimino_return: harddrop_tetrimino: lda newlyPressedButtons and #BUTTON_UP+BUTTON_SELECT + +; secret grade checking deferred until frame following a harddrop + bne @hardDrop + lda secretGradePending beq playState_playerControlsActiveTetrimino_return + lda #0 + sta secretGradePending + jmp secretGradeGrading +@hardDrop: + lda #1 + sta secretGradePending lda tetriminoY sta tmpY lda hardDropGhostY ; value set by previous frame's sprite staging @@ -40,7 +49,8 @@ harddrop_tetrimino: rts @noSonic: - ; hard drop + lda #$20 + sta vramRow lda #1 sta playState lda #0 @@ -101,6 +111,35 @@ harddropAddr = pointerAddr sta harddropAddr+3 harddropMarkCleared: +; check top row separately + lda teppozFlag + beq @checkTopRow + jmp harddropShift +@checkTopRow: + lda playfield + ora playfield+1 + ora playfield+2 + ora playfield+3 + ora playfield+4 + ora playfield+5 + ora playfield+6 + ora playfield+7 + ora playfield+8 + ora playfield+9 + bmi @normalBoardHandling + inc harddropBuffer ; mark top row as cleared + ldx #245 +@shiftPlayfield: + ; no page boundries crossed to avoid +1 cycle penalty + lda playfield,x + sta playfield+10,x + dex + ; loop stops at zero to avoid comparison + bne @shiftPlayfield + ; last tile omitted in loop, handle separately + lda playfield + sta playfield+10 +@normalBoardHandling: sec lda tetriminoY sbc #3 @@ -108,33 +147,33 @@ harddropMarkCleared: clc adc #4 sta tmpY ; row -@lineLoop: - ; A should always be tmpY - - tax - lda multBy10Table, x - sta harddropAddr - - ; check for empty row - ldy #$9 -@minoLoop: - lda (harddropAddr), y - bmi @noLineClear ; EMPTY_TILE sets negative flag, normal tiles do not - - dey - bpl @minoLoop - -@lineClear: - lda #1 - jmp @write -@noLineClear: + lda tmpX + bpl @lineLoop lda #0 -@write: - ; X should be tmpY - sta harddropBuffer, x - + sta tmpX ; sets lower limit to row 1 +@lineLoop: + lda #$13 + sec + sbc tmpY ; contains current row being checked + cmp currentFloor + bcc @skipRow ; ignore floor rows + ldx tmpY + ldy multBy10Table, x + lda playfield,y + ora playfield+1,y + ora playfield+2,y + ora playfield+3,y + ora playfield+4,y + ora playfield+5,y + ora playfield+6,y + ora playfield+7,y + ora playfield+8,y + ora playfield+9,y + eor #$80 + asl + rol harddropBuffer,x +@skipRow: dec tmpY - lda tmpY cmp tmpX bne @lineLoop @@ -145,9 +184,8 @@ harddropShift: adc #1 sta tmpY ; row @lineLoop: - ; A should always be tmpY - - tax + ldx tmpY + beq @noLineClear ; ignore top row lda harddropBuffer, x beq @noLineClear @@ -166,7 +204,6 @@ harddropShift: ldx tmpY @offsetLoop: dex - lda harddropBuffer, x bne @lineIsFull dec completedLinesCopy @@ -200,33 +237,47 @@ harddropShift: @nextLine: dec tmpY - lda tmpY beq @addScore jmp @lineLoop - - @addScore: + lda harddropBuffer + beq @noTopRowClear + inc completedLines +@noTopRowClear: lda completedLines beq @noScore - jsr playState_updateLinesAndStatistics - lda #0 - sta vramRow - sta completedLines - ; emty top row - lda #EMPTY_TILE - ldx #9 + +; refresh rows * completed lines + ldy completedLines + ldx multBy10Table,y + dex + ldy #9 @topRowLoop: + lda topRowBuffer,y sta playfield, x + dey + bpl @noReset + ldy #9 +@noReset: dex bpl @topRowLoop + jsr drawFloorTopRow + jsr playState_updateLinesAndStatistics + + lda #0 + sta vramRow + ; lda #TETRIMINO_X_HIDE ; sta tetriminoX + jsr stageFullPlayfield @noScore: lda #8 ; jump straight to spawnTetrimino sta playState + lda #PIECE_HIDDEN + sta currentPiece lda dropSpeed sta fallTimer lda #$7 @@ -277,14 +328,13 @@ rotationTable: .dbyt $0705,$0406,$0507,$0604 .dbyt $0909,$0808,$0A0A,$0C0C .dbyt $0B0B,$100E,$0D0F,$0E10 - .dbyt $0F0D,$1212,$1111 + .dbyt $0F0D,$1212,$1111,$1313 drop_tetrimino: lda linecapState cmp #LINECAP_KILLX2 beq @killX2 - lda practiseType - cmp #MODE_KILLX2 - bne @normal + lda killX2Flag + beq @normal @killX2: jsr lookupDropSpeed sta tmpY @@ -411,31 +461,19 @@ shift_tetrimino: rts @dasOnlyEnd: - lda practiseType - cmp #MODE_DAS - bne @normalDAS - lda dasModifier - sta dasValueDelay - lda palFlag - eor #1 - asl - adc #$8 - sta dasValuePeriod - jmp @shiftTetrimino -@normalDAS: - ; region stuff - lda #$10 - sta dasValueDelay - lda #$A - sta dasValuePeriod - ldy palFlag - ; cpy #0 ; ldy sets z flag - beq @shiftTetrimino - lda #$0C + lda dasModifier sta dasValueDelay - lda #$08 + sec + sbc arrModifier sta dasValuePeriod + ; ldy palFlag + ; ; cpy #0 ; ldy sets z flag + ; beq @shiftTetrimino + ; lda #PAL_DAS + ; sta dasValueDelay + ; lda #PAL_DAS - PAL_ARR + ; sta dasValuePeriod @shiftTetrimino: lda tetriminoX @@ -449,15 +487,22 @@ shift_tetrimino: lda heldButtons and #$03 beq @ret + lda disableDasFlag + bne @ret inc autorepeatX lda autorepeatX cmp dasValueDelay bmi @ret +@zeroDas: lda dasValuePeriod + cmp dasValueDelay + beq @zeroArr sta autorepeatX jmp @buttonHeldDown @resetAutorepeatX: + lda dasValueDelay + beq @zeroDas lda #$00 sta autorepeatX @buttonHeldDown: @@ -485,6 +530,42 @@ shift_tetrimino: @restoreX: lda originalY sta tetriminoX + lda noWallChargeFlag + bne @ret lda dasValueDelay sta autorepeatX @ret: rts + +@zeroArr: + lda heldButtons + and #BUTTON_RIGHT + beq @checkLeftPressed +@shiftRight: + inc tetriminoX + jsr isPositionValid + bne @shiftBackToLeft + lda #$03 + sta soundEffectSlot1Init + jmp @shiftRight +@checkLeftPressed: + lda heldButtons + and #BUTTON_LEFT + beq @leftNotPressed +@shiftLeft: + dec tetriminoX + jsr isPositionValid + bne @shiftBackToRight + lda #$03 + sta soundEffectSlot1Init + jmp @shiftLeft +@shiftBackToLeft: + dec tetriminoX + dec tetriminoX +@shiftBackToRight: + inc tetriminoX + lda noWallChargeFlag + bne @leftNotPressed + lda dasValueDelay + sta autorepeatX +@leftNotPressed: + rts diff --git a/src/playstate/branch.asm b/src/playstate/branch.asm index aa5b982d..e92bc3a6 100644 --- a/src/playstate/branch.asm +++ b/src/playstate/branch.asm @@ -15,7 +15,7 @@ branchOnPlayStatePlayer1: playState_incrementPlayState playState_unassignOrientationId: - lda #$13 + lda #PIECE_HIDDEN sta currentPiece rts @@ -32,5 +32,6 @@ playState_noop: .include "garbage.asm" .include "spawnnext.asm" .include "gameover_rocket.asm" +.include "trt.asm" .include "util.asm" diff --git a/src/playstate/completedrows.asm b/src/playstate/completedrows.asm index 53d57272..bf869585 100644 --- a/src/playstate/completedrows.asm +++ b/src/playstate/completedrows.asm @@ -1,5 +1,3 @@ -activeFloorMode := generalCounter5 - playState_checkForCompletedRows: lda vramRow cmp #$20 @@ -7,6 +5,7 @@ playState_checkForCompletedRows: jmp playState_checkForCompletedRows_return @updatePlayfieldComplete: + @currentRow = generalCounter2 lda tetriminoY sec @@ -16,7 +15,7 @@ playState_checkForCompletedRows: @yInRange: clc adc lineIndex - sta generalCounter2 + sta @currentRow asl a sta generalCounter asl a @@ -25,37 +24,31 @@ playState_checkForCompletedRows: adc generalCounter sta generalCounter tay - lda #$00 - sta activeFloorMode ; Don't draw floor unless active ldx #$0A @checkIfRowComplete: .if AUTO_WIN jmp @rowIsComplete .endif + lda teppozFlag + bne @rowNotComplete + lda practiseType cmp #MODE_TSPINS beq @rowNotComplete ; lda practiseType ; accumulator is still practiseType - cmp #MODE_FLOOR - beq @floorCheck + lda floorModifier + bne @fullRowBurningCheck lda linecapState cmp #LINECAP_FLOOR - beq @fullRowBurningCheck - bne @normalRow - -@floorCheck: - lda currentFloor - beq @rowNotComplete + bne @checkIfRowCompleteLoopStart @fullRowBurningCheck: - inc activeFloorMode ; Floor is active lda #$13 sec - sbc generalCounter2 ; contains current row being checked + sbc @currentRow ; contains current row being checked cmp currentFloor bcc @rowNotComplete ; ignore floor rows -@normalRow: @checkIfRowCompleteLoopStart: lda (playfieldAddr),y @@ -69,7 +62,7 @@ playState_checkForCompletedRows: ; sound effect $A to slot 1 used to live here inc completedLines ldx lineIndex - lda generalCounter2 + lda @currentRow sta completedRow,x ldy generalCounter dey @@ -83,32 +76,13 @@ playState_checkForCompletedRows: dey cpy #$FF bne @movePlayfieldDownOneRow - lda #EMPTY_TILE - ldy #$00 -@clearRowTopRow: - sta (playfieldAddr),y - iny - cpy #$0A - bne @clearRowTopRow - lda #$13 - sta currentPiece -; draw surface of floor in case of top line clear - lda activeFloorMode - beq @incrementLineIndex - lda #$14 - sec - sbc currentFloor - tax - ldy multBy10Table,x - ldx #$0A - lda #BLOCK_TILES+3 -@drawFloorSurface: - sta playfield,y - iny - dex - bne @drawFloorSurface + jsr refreshTopRow + lda #PIECE_HIDDEN + sta currentPiece +; draw surface of floor in case of top line clear + jsr drawFloorTopRow jmp @incrementLineIndex @rowNotComplete: @@ -122,14 +96,11 @@ playState_checkForCompletedRows: cmp #MODE_TAPQTY bne @tapQtyEnd lda completedLines - ; cmp #0 ; lda sets z flag beq @tapQtyEnd ; mark as complete lda tqtyNext sta tqtyCurrent - ; handle no burns - lda tapqtyModifier - and #$F0 + lda noLineClearDelayFlag beq @tapQtyEnd lda #0 sta vramRow @@ -140,13 +111,6 @@ playState_checkForCompletedRows: rts @tapQtyEnd: - ; update top row for crunch - lda practiseType - cmp #MODE_CRUNCH - bne @crunchEnd - jsr advanceSides ; clobbers generalCounter3 and generalCounter4 -@crunchEnd: - lda completedLines beq :+ lda #$0A diff --git a/src/playstate/gameover_rocket.asm b/src/playstate/gameover_rocket.asm index c79e4df2..14f75faa 100644 --- a/src/playstate/gameover_rocket.asm +++ b/src/playstate/gameover_rocket.asm @@ -21,7 +21,7 @@ playState_checkStartGameOver: tay lda #$00 sta generalCounter3 - lda #$13 + lda #PIECE_HIDDEN sta currentPiece @drawCurtainRow: lda #$4F @@ -62,8 +62,8 @@ playState_checkStartGameOver: @checkForStartButton: lda newlyPressedButtons_player1 - cmp #$10 - bne @ret2 + and #BUTTON_START + beq @ret2 @exitGame: lda #$00 sta playState @@ -78,19 +78,17 @@ sleep_gameplay: rts endingAnimation: ; rocket_screen - jsr updateAudioWaitForNmiAndDisablePpuRendering - jsr disableNmi + jsr hideSpritesAndBackground .if INES_MAPPER <> 0 ; NROM will use a smaller ufo in the game tileset lda #CHRBankSet1 jsr changeCHRBanks .endif - lda #NMIEnable - sta currentPpuCtrl + ldx #RLE_NT_ROCKET jsr copyRleNametableToPpu - .addr rocket_nametable - jsr bulkCopyToPpu - .addr rocket_palette + + stagePatch rocketPalette + jsr render_mode_queue ; lines lda #$21 @@ -133,17 +131,16 @@ endingAnimation: ; rocket_screen lda levelNumber jsr renderByteBCDNoPad - jsr waitForVBlankAndEnableNmi - jsr updateAudioWaitForNmiAndResetOamStaging - jsr updateAudioWaitForNmiAndEnablePpuRendering -.if INES_MAPPER <> 3 - jsr updateAudioWaitForNmiAndResetOamStaging -.endif +; reenable display + jsr resetScroll + lda #NMIEnable + sta currentPpuCtrl + lda #RENDER_ROCKET + sta renderMode + jsr showSpriteAndBackground lda #0 sta screenStage - lda #$5 - sta renderMode lda #$1 sta endingSleepCounter lda #$80 ; timed in bizhawk tasstudio to be 1 frame longer than usual (probably a lag frame) @@ -153,6 +150,8 @@ endingLoop: jsr updateAudioWaitForNmiAndResetOamStaging jsr handleRocket + jsr showQualWait + lda screenStage bne @waitEnd diff --git a/src/playstate/garbage.asm b/src/playstate/garbage.asm index 39d2dd19..e904ea10 100644 --- a/src/playstate/garbage.asm +++ b/src/playstate/garbage.asm @@ -1,4 +1,5 @@ playState_receiveGarbage: + jsr secretGradeGrading ldy pendingGarbage beq @ret lda multBy10Table,y diff --git a/src/playstate/lock.asm b/src/playstate/lock.asm index f30e0aa5..9429c3ff 100644 --- a/src/playstate/lock.asm +++ b/src/playstate/lock.asm @@ -1,8 +1,10 @@ playState_lockTetrimino: +VITS_SCORE = 100000 @currentTile = generalCounter5 jsr isPositionValid beq @notGameOver @gameOver: + inc gameTimerStop lda practiseType cmp #MODE_TYPEB bne @revealScore @@ -45,14 +47,50 @@ playState_lockTetrimino: @notGameOver: lda vramRow cmp #$20 - bmi @ret + bpl @noWait + rts +@noWait: ldy tetriminoY lda multBy10Table,y clc adc tetriminoX sta generalCounter + +; score if vits + ldx vitsScoreFlag + beq @noVits + ldx currentPiece + cpx #PIECE_I_VERT + bne @noVits + ; check if tile exists above + sec + sbc #30 + tax + lda playfield,x + bmi @noVits + ; tile exists + clc + lda #VITS_SCORE + adc binScore+1 + sta binScore+1 + lda #^VITS_SCORE + adc binScore+2 + sta binScore+2 + lda #0 + adc binScore+3 + sta binScore+3 +@noVits: ldx currentPiece + lda #EMPTY_TILE + ldy practiseType + cpy #MODE_TAP + beq @storeTile +;normal tile lda tetriminoTileFromOrientation,x +@storeTile: sta @currentTile txa asl a @@ -82,7 +120,7 @@ playState_lockTetrimino: cmp #MODE_LOWSTACK bne @notAboveLowStack jsr checkIfAboveLowStackLine - bcc @notAboveLowStack + bmi @notAboveLowStack ldx #lowStackNopeGraphic sec @@ -104,4 +142,4 @@ playState_lockTetrimino: jsr updatePlayfield jsr updateMusicSpeed inc playState -@ret: rts + jmp secretGradeGrading diff --git a/src/playstate/spawnnext.asm b/src/playstate/spawnnext.asm index 3cd6fcc2..393fc981 100644 --- a/src/playstate/spawnnext.asm +++ b/src/playstate/spawnnext.asm @@ -1,6 +1,8 @@ SPAWN_NEXT_ADDONS := 1 playState_spawnNextTetrimino: + lda hardDropFlag + bne :+ lda vramRow cmp #$20 bpl :+ @@ -52,6 +54,12 @@ playState_spawnNextTetrimino: jsr incrementPieceStat jsr chooseNextTetrimino sta nextPiece + ldx entryChargeModifier + beq @resetDownHold + dex + bne @resetDownHold ; kitaru charge handled in playstate branch + lda dasModifier + sta autorepeatX ; store full charge for hydrant/1 @resetDownHold: lda #$00 sta autorepeatY @@ -71,7 +79,7 @@ pickRandomTetrimino: tax lda spawnTable,x cmp spawnID - bne useNewSpawnID + bne @useNewSpawnID @invalidIndex: ldx #rng_seed jsr generateNextPseudorandomNumber @@ -79,44 +87,55 @@ pickRandomTetrimino: and #$07 clc adc spawnID -L992A: cmp #$07 - bcc L9934 +; check if valid before checking palpepFlag + cmp #$07 + bcc @valid + ldx palpepFlag + bne @longbar +; mod7 loop only if not palpep +@mod7: cmp #$07 + bcc @valid sec sbc #$07 - jmp L992A - -L9934: tax + jmp @mod7 +@valid: tax lda spawnTable,x -useNewSpawnID: +@useNewSpawnID: sta spawnID - jsr pickTetriminoPost - rts + jmp pickTetriminoPost +@longbar: + lda #PIECE_I_HORIZ + bne @useNewSpawnID pickTetriminoPre: lda practiseType cmp #MODE_TSPINS beq pickTetriminoT - ; lda practiseType ; accumulator is still practiseType - cmp #MODE_SEED - beq pickTetriminoSeed - ; lda practiseType cmp #MODE_TAPQTY beq pickTetriminoLongbar - ; lda practiseType cmp #MODE_TAP beq pickTetriminoLongbar - ; lda practiseType cmp #MODE_PRESETS - beq pickTetriminoPreset + bne @notPreset + jmp pickTetriminoPreset +@notPreset: + lda seedEnabled + beq pickRandomTetrimino + lda seedEnabled + beq @pickRandomTetrimino + lda seededPieces + bne pickTetriminoSeed +@pickRandomTetrimino: jmp pickRandomTetrimino pickTetriminoT: - lda #$2 + lda #PIECE_T_DOWN sta spawnID rts pickTetriminoLongbar: - lda #$12 + ldx practisePiece + lda spawnTable,x sta spawnID rts @@ -169,19 +188,28 @@ pickTetriminoSeed: and #$07 clc adc spawnID -@L992A: + +; check if valid before checking palep cmp #$07 - bcc @L9934 + bcc @valid + ldx palpepFlag + bne @longbar +; mod7 only if not palpep +@mod7: + cmp #$07 + bcc @valid sec sbc #$07 - jmp @L992A - -@L9934: + jmp @mod7 +@valid: tax lda spawnTable,x @useNewSpawnID: sta spawnID - rts + jmp pickTetriminoPost +@longbar: + lda #PIECE_I_HORIZ + bne @useNewSpawnID setSeedNextRNG: ldx #set_seed @@ -225,13 +253,25 @@ pickTetriminoPost: cmp #MODE_DROUGHT beq pickTetriminoDrought lda spawnID ; restore A + ldx splitSquareFlag + beq @ret + cmp #PIECE_O + bne @ret + lda #PIECE_SPLIT_SQUARE + sta spawnID +@ret: rts pickTetriminoDrought: + ldx #rng_seed+1 + lda seedEnabled + beq @notSeeded + ldx #set_seed+1 +@notSeeded: lda spawnID ; restore A - cmp #$12 + cmp #PIECE_I_HORIZ bne @droughtDone - lda rng_seed+1 + lda $00,x and #$F adc #1 ; always adds 1 so code continues as normal if droughtModifier is 0 cmp droughtModifier @@ -240,4 +280,8 @@ pickTetriminoDrought: @droughtDone: rts @pickRando: + lda seedEnabled + beq @vanillaRng + jmp pickTetriminoSeed +@vanillaRng: jmp pickRandomTetrimino diff --git a/src/playstate/trt.asm b/src/playstate/trt.asm new file mode 100644 index 00000000..4584eeba --- /dev/null +++ b/src/playstate/trt.asm @@ -0,0 +1,177 @@ +trtCalculate: + +; fix for now so that trt rate & trans mode work okay together + lda completedLines + clc + adc trtLines + sta trtLines + and #$0F + cmp #$A + bcc @noLinesCarry + lda trtLines + clc + adc #$06 + sta trtLines + cmp #$A0 + bcc @noLinesCarry + and #$0F + sta trtLines + inc trtLines+1 +@noLinesCarry: + ldx completedLines + cpx #$04 + bne @notTetris +@addToLineCounter: + inc trtLineCounter + lda trtLineCounter + and #$0F + cmp #$0A + bmi @noCarry + lda trtLineCounter + clc + adc #$06 + sta trtLineCounter + and #$F0 + cmp #$A0 + bcc @noCarry + lda trtLineCounter + and #$0F + sta trtLineCounter + inc trtLineCounter+1 +@noCarry: + dex + bne @addToLineCounter +@notTetris: + lda #$00 + ldx #$06 +@zeroScratch: + sta trtLineCounter+1,x + dex + bne @zeroScratch + ldy #$04 + lda trtLineCounter + sta trtRam+2 + lda trtLineCounter+1 + sta trtRam+1 +LFA41: clc + ldx #$03 +LFA44: rol trtRam,x + dex + bne LFA44 + dey + bne LFA41 +LFA4D: jsr LFADF + lda trtScratch+4 + cmp trtRam + bcc LFA6C + bne LFA85 + lda trtScratch+3 + cmp trtRam+1 + bcc LFA6C + bne LFA85 + lda trtScratch+2 + cmp trtRam+2 + bcs LFA85 +LFA6C: ldx #$03 +LFA6E: lda trtScratch+1,x + sta trtRam+3,x + dex + bne LFA6E + lda trtScratch+5 + adc #$10 + sta trtScratch+5 + cmp #$A0 + bne LFA4D + beq LFABD +LFA85: ldx #$03 +LFA87: lda trtRam+3,x + sta trtScratch+1,x + dex + bne LFA87 + ldy #$04 +LFA92: asl trtScratch+2 + rol trtScratch+3 + rol trtScratch+4 + dey + bne LFA92 +LFA9E: jsr LFADF + inc trtScratch+5 + lda trtScratch+4 + cmp trtLineCounter+1 + bcc LFAB6 + bne LFABD + lda trtScratch+3 + cmp trtLineCounter + bcs LFABD +LFAB6: lda trtScratch+5 + cmp #$A0 + bne LFA9E +LFABD: lda trtScratch+5 + and #$0F + cmp #$0A + bne LFACE + lda trtScratch+5 + adc #$05 + sta trtScratch+5 +LFACE: lda trtScratch+5 + cmp #$01 + bne LFADA + lda #$00 + sta trtScratch+5 +LFADA: + ; lda holdDownPoints + ; cmp #$02 + rts + +LFADF: lda trtLines + and #$0F + sta trtScratch+1 + lda trtScratch+2 + and #$0F + clc + adc trtScratch+1 + tax + lda trtScratch+2 + and #$F0 + adc trtBCDTable,x + cmp #$A0 + bcc LFB01 + adc #$5F + inc trtScratch+3 +LFB01: sta trtScratch+2 + lda trtLines + and #$F0 + sta trtScratch+1 + lda trtScratch+2 + clc + adc trtScratch+1 + bcc LFB19 + inc trtScratch+3 + adc #$5F +LFB19: cmp #$A0 + bcc LFB22 + adc #$5F + inc trtScratch+3 +LFB22: sta trtScratch+2 + lda trtScratch+3 + clc + adc trtLines+1 + sta trtScratch+3 + and #$0F + cmp #$0A + bcc LFB3C + lda trtScratch+3 + adc #$05 + sta trtScratch+3 +LFB3C: lda trtScratch+3 + cmp #$A0 + bcc LFB48 + adc #$5F + inc trtScratch+4 +LFB48: sta trtScratch+3 + rts + +trtBCDTable: + .byte $00,$01,$02,$03,$04,$05,$06,$07 + .byte $08,$09,$10,$11,$12,$13,$14,$15 + .byte $16,$17,$18 diff --git a/src/playstate/updatestats.asm b/src/playstate/updatestats.asm index 83b07592..b35e34e2 100644 --- a/src/playstate/updatestats.asm +++ b/src/playstate/updatestats.asm @@ -28,7 +28,7 @@ playState_updateLinesAndStatistics: sec sbc generalCounter sta lines - bpl @checkForBorrow + bcs @checkForBorrow lda #$00 sta lines jmp addPoints @@ -79,23 +79,16 @@ checkLevelUp: bne @lineLoop iny ; used by floorcap check below + lda sxtoklFlag + bne @nextLevel lda practiseType cmp #MODE_TAPQTY beq @lineLoop cmp #MODE_MARATHON bne @notMarathon - lda marathonModifier - beq @lineLoop ; marathon mode 0 does not transition - bne @notSXTOKL + lda marathonLevelModifier + beq @lineLoop @notMarathon: - cmp #MODE_TRANSITION - bne @notSXTOKL - lda transitionModifier - cmp #$10 - bne @notSXTOKL - jmp @nextLevel -@notSXTOKL: - lda lines+1 sta generalCounter2 lda lines @@ -131,22 +124,21 @@ checkLevelUp: checkLinecap: ; set linecapState ; check if enabled - lda linecapFlag + ldx linecapWhen beq @linecapEnd - ; skip check if already set lda linecapState bne @linecapEnd - - lda linecapWhen + dex beq @linecapLevelCheck ;linecapLinesCheck +@linecapLines: lda lines+1 - cmp linecapLines+1 + cmp linecapLines bcc @linecapEnd lda lines - cmp linecapLines + cmp linecapLines+1 bcc @linecapEnd bcs @linecapApply @@ -182,6 +174,10 @@ checkLinecap: ; set linecapState @floorLinecapEnd: addPoints: + lda trtFlag + beq @noTetrisRate + jsr trtCalculate +@noTetrisRate: inc playState lda practiseType cmp #MODE_CHECKERBOARD @@ -211,7 +207,6 @@ addPointsRaw: .if NO_SCORING rts .endif - lda holdDownPoints cmp #$02 bmi @noPushDown @@ -349,9 +344,8 @@ addLineClearPoints: ldy practiseType cpy #MODE_MARATHON bne @notMarathon - ldy marathonModifier - cpy #3 ; Marathon modes 3 + 4 score normally - bcs @notMarathon + ldy marathonScoreFlag + bne @notMarathon lda startLevel @notMarathon: sta factorA24+0 diff --git a/src/playstate/util.asm b/src/playstate/util.asm index 7f7a8311..494e669a 100644 --- a/src/playstate/util.asm +++ b/src/playstate/util.asm @@ -65,21 +65,17 @@ updatePlayfield: crunchLeftColumns = generalCounter3 crunchRightColumns = generalCounter4 -updateMusicSpeed: - - ; ldx #$05 - ; lda multBy10Table,x ;this piece of code is parameterized for no reason but the crash checking code relies on the index being 50-59 so if you ever optimize this part out of the code please also adjust the crash test, specifically the part which handles cycles for allegro. - ; tay - - ldy #50 ; replaces above - -; check if crunch mode - ldx practiseType - cpx #MODE_CRUNCH - bne @notCrunch +crunchAdjustYSetX: + ; add left columns to Y + ; set X to playable columns + ldx crunchLeftModifier + bne @crunch + ldx crunchRightModifier + beq @notCrunch +@crunch: ; add crunch left columns to y - jsr unpackCrunchModifier + jsr copyCrunchModifier tya clc adc crunchLeftColumns ; offset y with left column count (generalCounter3) @@ -91,17 +87,36 @@ updateMusicSpeed: sbc crunchLeftColumns ; generalCounter3 sbc crunchRightColumns ; generalCounter4 tax - bne @checkForBlockInRow ; unconditional, expected range 4 - 10 - + bne @ret ; unconditional, expected range 4 - 10 @notCrunch: ldx #$0A +@ret: + rts + +isBlockInRowAtY: +; y = start of row +; negative flag clear if block found +; check if crunch mode + jsr crunchAdjustYSetX @checkForBlockInRow: - lda (playfieldAddr),y - cmp #EMPTY_TILE - bne @foundBlockInRow + lda playfield,y + bpl @foundBlock iny dex bne @checkForBlockInRow + lda #$80 ; set negative flag +@foundBlock: + rts + +updateMusicSpeed: + + ; ldx #$05 + ; lda multBy10Table,x ;this piece of code is parameterized for no reason but the crash checking code relies on the index being 50-59 so if you ever optimize this part out of the code please also adjust the crash test, specifically the part which handles cycles for allegro. + ; tay + + ldy #50 ; replaces above + jsr isBlockInRowAtY + bpl @foundBlockInRow lda allegro sta wasAllegro beq @ret @@ -128,23 +143,13 @@ updateMusicSpeed: @ret: rts checkIfAboveLowStackLine: -; carry set - block found +; negative clear - block found sec lda #19 sbc lowStackRowModifier tax ldy multBy10Table,x - ldx #$0A - sec -@checkForBlockInRow: - lda playfield,y - bpl @foundBlockInRow - iny - dex - bne @checkForBlockInRow - clc -@foundBlockInRow: - rts + jmp isBlockInRowAtY ; canon is adjustMusicSpeed setMusicTrack: diff --git a/src/ram.asm b/src/ram.asm index 4015c984..f6c9d3e5 100644 --- a/src/ram.asm +++ b/src/ram.asm @@ -6,14 +6,14 @@ tmpX: .res 1 ; $0003 tmpY: .res 1 ; $0004 tmpZ: .res 1 ; $0005 -tmpBulkCopyToPpuReturnAddr: .res 2 ; $0006 ; 2 bytes +.res 2 binScore: .res 4 ; $8 ; 4 bytes binary score: .res 4 ; $C ; 4 bytes BCD nmiReturnAddr: .res 1 ; $0010 ; used for crash crashState: .res 1 ; $0011 ; used for crash cycleCount: .res 2 ; $0012 ; 2 bytes ; used for crash oneThirdPRNG: .res 1 ; $0014 ; used for crash - .res $2 +b_seed: .res 2 ; loaded with rng_seed unless seeded rng_seed: .res 2 ; $0017 spawnID: .res 1 ; $0019 @@ -24,13 +24,16 @@ allegroIndex: .res 1 ; $001F for crash wasAllegro: .res 1 ; $0020 for crash startParity: .res 1 ; $0021 for crash lagState: .res 1 ; $0022 for lagged lines & score - .res $10 + .res $F +mainLoopWait: .res 1 ; $0032 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 - +patchPtr: .res 2 +.res 1 +.res 4 +renderQueueLength: .res 1 +renderQueuePointer: .res 1 tetriminoX: .res 1 ; $0040 tetriminoY: .res 1 ; $0041 currentPiece: .res 1 ; $0042 ; Current piece as an orientation ID @@ -51,7 +54,7 @@ linesTileQueue: .res 1 ; $54 .res 1 completedLines: .res 1 ; $0056 lineIndex: .res 1 ; $0057 ; Iteration count of playState_checkForCompletedRows -startHeight: .res 1 ; $0058 +.res 1 garbageHole: .res 1 ; $0059 ; Position of hole in received garbage garbageDelay: .res 1 ; $005A pieceTileModifier: .res 1 ; $005B ; above $80 - use a single one, below - use an offset @@ -76,12 +79,59 @@ 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 +pageItemCount: .res 1 +digitPtr: .res $2 +originalPage: .res $1 +nybbleTemp: .res $1 +blankCounter: .res $1 +rowCounter: .res $1 + +.res 4 + +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 -stringIndexLookup: -spriteIndexInOamContentLookup: .res 1 ; $00A2 +stringAttrib: .res 1 ; $00A2 +stringLength: .res 1 ; $00A2 +stringIndex: +spriteIndex: .res 1 ; can probably be the same as stringIndex renderFlags: .res 1 ; $00A3 ; gameplay ; Bit 0-lines 1-level 2-score 3-debug 4-hz 6-stats 7-high score entry letter @@ -90,7 +140,7 @@ renderFlags: .res 1 ; $00A3 ; level menu ; 0-customLevel - .res $3 + .res $1 gameModeState: .res 1 ; $00A7 ; For values, see playState_checkForCompletedRows generalCounter: .res 1 ; $00A8 ; canon is legalScreenCounter2 @@ -123,7 +173,9 @@ endingSleepCounter: .res 2 ; $00C4 endingRocketCounter: .res 1 ; $00C6 endingRocketX: .res 1 ; $C7 endingRocketY: .res 1 ; $C8 - .res 5 +gameTimer: .res 2 +gameTimerStop: .res 1 + .res 2 .res 6 ; used to be demo stuff highScoreEntryNameOffsetForLetter: .res 1 ; $00D4 ; Relative to current row highScoreEntryRawPos: .res 1 ; $00D5 ; High score position 0=1st type A, 1=2nd... 4=1st type B... 7=4th/extra type B @@ -144,14 +196,11 @@ soundRngSeed: .res 2 ; $00EB ; Set, but not read currentSoundEffectSlot: .res 1 ; $00ED ; Temporary musicChannelOffset: .res 1 ; $00EE ; Temporary. Added to $4000-3 for MMIO currentAudioSlot: .res 1 ; $00EF ; Temporary - .res 1 -unreferenced_buttonMirror: .res 3 ; $00F1 ; Mirror of $F5-F8 - .res 1 + .res 5 newlyPressedButtons_player1: .res 1 ; $00F5 ; $80-a $40-b $20-select $10-start $08-up $04-down $02-left $01-right newlyPressedButtons_player2: .res 1 ; $00F6 heldButtons_player1: .res 1 ; $00F7 -heldButtons_player2: .res 1 ; $00F8 - .res 2 + .res 3 joy1Location: .res 1 ; $00FB ; normal=0; 1 or 3 for expansion ppuScrollY: .res 1 ; $00FC ppuScrollX: .res 1 ; $00FD @@ -162,15 +211,20 @@ currentPpuCtrl: .res 1 ; $00FF stack: .res $FF ; $0100 .res 1 oamStaging: .res $100 ; $0200 ; format: https://wiki.nesdev.com/w/index.php/PPU_programmer_reference#OAM - .res $F0 + + +; todo: find out which need to be preserved +trtLineCounter: .res $2 +trtScratch: .res $6 +trtRam: .res $8 + .res $E0 statsByType: .res $E ; $03F0 .res 2 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 @@ -219,8 +273,11 @@ invisibleFlag: .res 1 ; $63B ; 0 for normal mode, non-zero for Invisible playfi currentFloor: .res 1 ; $63C floorModifier is copied here at game init. Set to 0 otherwise and incremented when linecap floor. mapperId: .res 1 ; $63D ; For INES_MAPPER 1000 (autodetect). 0 = CNROM. 1 = MMC1. hardDropGhostY: .res 1 ; ghost Y used as a shortcut for hard/sonic drop +anydasFlag: .res 1 +seededPieces: .res 1 +killX2Flag: .res 1 +.res 1 -.if KEYBOARD kbReadState: .res 1 ; $063F - used for high score entry kbHeldInput: .res 1 ; $0640 - high score input throttling kbRawInput: .res 9 ; $0641 - all 72 keys' input @@ -228,12 +285,19 @@ kbRawInput: .res 9 ; $0641 - all 72 keys' input ; used to track state of high score entry screen. Can possibly use the address of the nmi interrupted ; routine in the stack to track instead highScoreEntryActive: .res 1 ; $064A -.else - .res $C -.endif +trtLines: .res 2 ; fix for now for transition mode/trt compat - .res $35 +menuStack: .res 32 +; only important in menu mode +prevGoofy: .res 1 + +topRowBuffer: .res 10 + +secretGrade: .res 1 +secretGradePending: .res 1 + +.segment "MUSIC_RAM": absolute musicStagingSq1Lo: .res 1 ; $0680 musicStagingSq1Hi: .res 1 ; $0681 audioInitialized: .res 1 ; $0682 @@ -297,8 +361,10 @@ soundEffectSlot2Playing: .res 1 ; $06FA soundEffectSlot3Playing: .res 1 ; $06FB soundEffectSlot4Playing: .res 1 ; $06FC currentlyPlayingMusicTrack: .res 1 ; $06FD ; Copied from musicTrack - .res 1 -unreferenced_soundRngTmp: .res 1 ; $06FF + .res 2 + + +.segment "SCORE_RAM": absolute highscores: ; $700 ; scores are name - score - lines - startlevel - level highScoreQuantity := 3 @@ -311,51 +377,28 @@ highScoreLength := highScoreNameLength + highScoreScoreLength + highScoreLinesLe .res 43 initMagic: .res 5 ; $075B ; Initialized to a hard-coded number. When resetting, if not correct number then it knows this is a cold boot + +.segment "VARS_RAM": absolute menuRAM: ; $760 -menuSeedCursorIndex: .res 1 -menuScrollY: .res 1 menuMoveThrottle: .res 1 menuThrottleTmp: .res 1 levelControlMode: .res 1 -customLevel: .res 1 classicLevel: .res 1 heartsAndReady: .res 1 ; high nybble used for ready -linecapCursorIndex: .res 1 -linecapWhen: .res 1 -linecapHow: .res 1 -linecapLevel: .res 1 -linecapLines: .res 2 -menuVars: ; $76E -paceModifier: .res 1 -presetModifier: .res 1 -typeBModifier: .res 1 -floorModifier: .res 1 -crunchModifier: .res 1 -tapModifier: .res 1 -transitionModifier: .res 1 -marathonModifier: .res 1 -tapqtyModifier: .res 1 -checkerModifier: .res 1 -garbageModifier: .res 1 -droughtModifier: .res 1 -dasModifier: .res 1 -lowStackRowModifier: .res 1 -scoringModifier: .res 1 -crashModifier: .res 1 -strictFlag: .res 1 ;used for crash detection. If 1, the game will register a crash anytime there is a possibility of one. -hzFlag: .res 1 -inputDisplayFlag: .res 1 -disableFlashFlag: .res 1 -disablePauseFlag: .res 1 -darkModifier: .res 1 -goofyFlag: .res 1 -debugFlag: .res 1 -linecapFlag: .res 1 -dasOnlyFlag: .res 1 -qualFlag: .res 1 -palFlag: .res 1 -.if KEYBOARD = 1 -keyboardFlag: .res 1 -.endif +practiseType: .res 1 +customLevel: .res 1 + +; menu +activeMenu: .res 1 +activePage: .res 1 +activeRow: .res 1 +activeColumn: .res 1 +menuStackPtr: .res 1 + +menuVars: +.include "menu/menuram.asm" +sramVariableLength := * - menuVars +menuRAMLength = * - menuRAM + ; ... $7FF diff --git a/src/reset.asm b/src/reset.asm index 2ddc6496..39a0e5a3 100644 --- a/src/reset.asm +++ b/src/reset.asm @@ -16,9 +16,9 @@ reset: cld @clrmem: sta $0000,x sta $0100,x - sta $0200,x + ; oam staging cleared separately sta $0300,x - sta $0400,x + ; playfield cleared separately sta $0500,x sta $0600,x inx @@ -31,6 +31,17 @@ reset: cld dex ; $FF for stack pointer txs + jsr clearPlayfield + jsr resetOAMStaging + jsr drawBlackBGPalette + + lda #NMIEnable + sta currentPpuCtrl + sta PPUCTRL + lda #RENDER_QUEUE + sta renderMode + jsr waitForNmi + jsr hideSpritesAndBackground jsr mapperInit jsr setHorizontalMirroring .if INES_MAPPER <> 0 diff --git a/src/seeds.asm b/src/seeds.asm new file mode 100644 index 00000000..58177308 --- /dev/null +++ b/src/seeds.asm @@ -0,0 +1,17 @@ +checkIfSeeded: + lda #$00 + sta seededPieces + lda seedEnabled + beq @noSeed + lda practiseType + cmp #MODE_TSPINS + beq @noSeed + cmp #MODE_TAPQTY + beq @noSeed + cmp #MODE_TAP + beq @noSeed + cmp #MODE_PRESETS + beq @noSeed + inc seededPieces +@noSeed: + rts 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..5ce57d96 100644 --- a/src/sprites/loadsprite.asm +++ b/src/sprites/loadsprite.asm @@ -2,7 +2,7 @@ loadSpriteIntoOamStaging: clc - lda spriteIndexInOamContentLookup + lda spriteIndex rol a tax lda oamContentLookup,x @@ -43,83 +43,127 @@ loadSpriteIntoOamStaging: @ret: rts +.enum +SPRITE_LEVELSELECTCURSOR +SPRITE_GAMETYPECURSOR +SPRITE_MENUSTARTA +SPRITE_MENUSTARTB +SPRITE_BLANK +SPRITE_TPIECE +SPRITE_JPIECE +SPRITE_ZPIECE +SPRITE_OPIECE +SPRITE_SPIECE +SPRITE_LPIECE +SPRITE_IPIECE +SPRITE_SPLIT_SQUARE +SPRITE_HIGHSCORENAMECURSOR +SPRITE_DEBUGLEVELEDIT +SPRITE_STATESAVE +SPRITE_STATELOAD +SPRITE_HEARTCURSOR +SPRITE_HEART +SPRITE_READY +SPRITE_CUSTOMLEVELCURSOR +SPRITE_INGAMEHEART +SPRITE_SEEDCURSORA +SPRITE_SEEDCURSORB +SPRITE_PRACTISETYPECURSORA +SPRITE_PRACTISETYPECURSORB +SPRITE_MENUPAGESELECTA +SPRITE_MENUPAGESELECTB +.endenum + oamContentLookup: - .addr sprite00LevelSelectCursor - .addr sprite01GameTypeCursor - .addr sprite02Blank - .addr sprite02Blank - .addr sprite02Blank - .addr sprite02Blank - .addr sprite06TPiece - .addr sprite07JPiece - .addr sprite08ZPiece - .addr sprite09OPiece - .addr sprite0ASPiece - .addr sprite0BLPiece - .addr sprite0CIPiece - .addr sprite0EHighScoreNameCursor - .addr sprite0EHighScoreNameCursor - .addr sprite02Blank - .addr sprite02Blank - .addr sprite02Blank - .addr sprite02Blank - .addr sprite02Blank - .addr sprite02Blank - .addr sprite02Blank - .addr spriteDebugLevelEdit ; $16 - .addr spriteStateSave; $17 - .addr spriteStateLoad; $18 - .addr sprite02Blank ; $19 - .addr sprite02Blank ; $1A - .addr spriteSeedCursor ; $1B - .addr sprite02Blank - .addr spritePractiseTypeCursor ; $1D - .addr spriteHeartCursor ; $1E - .addr spriteHeart ; $1F - .addr spriteReady ; $20 - .addr spriteCustomLevelCursor ; $21 - .addr spriteIngameHeart ; $22 + .addr spriteLevelSelectCursor + .addr spriteGameTypeCursor + .addr spriteMenuStartOptionA + .addr spriteMenuStartOptionB + .addr spriteBlank + .addr spriteTPiece + .addr spriteJPiece + .addr spriteZPiece + .addr spriteOPiece + .addr spriteSPiece + .addr spriteLPiece + .addr spriteIPiece + .addr spriteSplitSquare + .addr spriteHighScoreNameCursor + .addr spriteDebugLevelEdit + .addr spriteStateSave + .addr spriteStateLoad + .addr spriteHeartCursor + .addr spriteHeart + .addr spriteReady + .addr spriteCustomLevelCursor + .addr spriteIngameHeart + .addr spriteSeedCursorA + .addr spriteSeedCursorB + .addr spritePractiseTypeCursorA + .addr spritePractiseTypeCursorB + .addr spriteMenuPageSelectA + .addr spriteMenuPageSelectB +; .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: +spriteLevelSelectCursor: .byte $00,$FC,$20,$00,$00,$FC,$20,$08 .byte $08,$FC,$20,$00,$08,$FC,$20,$08 .byte $FF -sprite01GameTypeCursor: +spriteGameTypeCursor: .byte $00,$27,$00,$00,$00,$27,$40,$3A .byte $FF -; Used as a sort of NOOP for cursors -sprite02Blank: +spriteMenuPageSelectA: + .byte $00,$27,$40,$00 + .byte $00,$27,$00,$D9 + .byte $FF +spriteMenuPageSelectB: + .byte $00,$27,$40,$02 + .byte $00,$27,$00,$D7 + .byte $FF +spriteMenuStartOptionA: + .byte $00,$96,$00,$FC + .byte $00,$97,$00,$04 + .byte $FF +spriteMenuStartOptionB: + .byte $00,$A6,$00,$FC + .byte $00,$A7,$00,$04 + .byte $FF +spriteBlank: .byte $00,$FF,$00,$00,$FF -sprite06TPiece: +spriteTPiece: .byte $00,$7B,$02,$FC,$00,$7B,$02,$04 .byte $00,$7B,$02,$0C,$08,$7B,$02,$04 .byte $FF -sprite07JPiece: +spriteJPiece: .byte $00,$7D,$02,$FC,$00,$7D,$02,$04 .byte $00,$7D,$02,$0C,$08,$7D,$02,$0C .byte $FF -sprite08ZPiece: +spriteZPiece: .byte $00,$7C,$02,$FC,$00,$7C,$02,$04 .byte $08,$7C,$02,$04,$08,$7C,$02,$0C .byte $FF -sprite09OPiece: +spriteOPiece: .byte $00,$7B,$02,$00,$00,$7B,$02,$08 .byte $08,$7B,$02,$00,$08,$7B,$02,$08 .byte $FF -sprite0ASPiece: +spriteSPiece: .byte $00,$7D,$02,$04,$00,$7D,$02,$0C .byte $08,$7D,$02,$FC,$08,$7D,$02,$04 .byte $FF -sprite0BLPiece: +spriteLPiece: .byte $00,$7C,$02,$FC,$00,$7C,$02,$04 .byte $00,$7C,$02,$0C,$08,$7C,$02,$FC .byte $FF -sprite0CIPiece: +spriteIPiece: .byte $04,$7B,$02,$F8,$04,$7B,$02,$00 .byte $04,$7B,$02,$08,$04,$7B,$02,$10 .byte $FF -sprite0EHighScoreNameCursor: +spriteSplitSquare: + .byte $00,$7B,$02,$FC,$00,$7B,$02,$0C + .byte $08,$7B,$02,$FC,$08,$7B,$02,$0C + .byte $FF +spriteHighScoreNameCursor: .byte $00,$FD,$20,$00,$FF spriteDebugLevelEdit: .byte $00,'X',$00,$00 @@ -134,11 +178,21 @@ spriteStateSave: .byte $00,'V',$03,$10,$00,'E',$03,$18 .byte $00,'D',$03,$20 .byte $FF -spriteSeedCursor: - .byte $00,$6B,$00,$00 +spriteSeedCursorA: + .byte $FD,$6B,$80,$00 + .byte $02,$6B,$00,$00 + .byte $FF +spriteSeedCursorB: + .byte $FC,$6B,$80,$00 + .byte $03,$6B,$00,$00 + .byte $FF +spritePractiseTypeCursorA: + .byte $00,$27,$40,$FB + .byte $00,$27,$00,$05 .byte $FF -spritePractiseTypeCursor: - .byte $00,$27,$00,$00 +spritePractiseTypeCursorB: + .byte $00,$27,$40,$FA + .byte $00,$27,$00,$06 .byte $FF spriteHeartCursor: .byte $00,$6c,$00,$00,$FF diff --git a/src/sprites/piece.asm b/src/sprites/piece.asm index 05e84a90..edabc240 100644 --- a/src/sprites/piece.asm +++ b/src/sprites/piece.asm @@ -1,17 +1,37 @@ stageSpriteForCurrentPiece: + jsr secretGradeSprite + lda gameTimerFlag + beq @noGameTimer + lda #$C0 + sta spriteXOffset + lda #$17 + sta spriteYOffset + lda #gameTimer + sta byteSpriteAddr + lda #0 + sta byteSpriteAddr+1 + lda #0 + sta byteSpriteTile + lda #2 + sta byteSpriteLen + jsr byteSprite + +@noGameTimer: lda #$0 sta pieceTileModifier + lda renderMode + cmp #9 + beq @skipCurrent jsr stageSpriteForCurrentPiece_actual - - lda practiseType - cmp #MODE_HARDDROP - beq ghostPiece +@skipCurrent: + lda hardDropFlag + bne ghostPiece + lda ghostPieceFlag + bne ghostPiece +@ret: rts ghostPiece: - lda playState - cmp #3 - bpl @noGhost lda tetriminoY sta tmp3 @loop: @@ -25,7 +45,18 @@ ghostPiece: ; check if equal to current position cmp tmp3 beq @noGhost - + lda ghostPieceFlag + beq @noGhost +; no ghost piece during entry delay + lda playState + cmp #1 + beq @ghost + cmp #8 + bne @noGhost +@ghost: + lda currentPiece + cmp #PIECE_HIDDEN + beq @noGhost lda frameCounter and #1 asl @@ -33,9 +64,9 @@ ghostPiece: adc #$0D sta pieceTileModifier jsr stageSpriteForCurrentPiece_actual +@noGhost: lda tmp3 sta tetriminoY -@noGhost: rts tileModifierForCurrentPiece: @@ -60,7 +91,9 @@ stageSpriteForCurrentPiece_actual: @currentTile = generalCounter5 lda tetriminoX cmp #TETRIMINO_X_HIDE - beq stageSpriteForCurrentPiece_return + bne @notHidden + rts +@notHidden: asl a asl a asl a @@ -90,8 +123,15 @@ stageSpriteForCurrentPiece_actual: asl a clc adc generalCounter4 - sta oamStaging,y sta originalY + sta oamStaging,y + lda mirrorVertFlag + beq @notMirrorVert + lda #$F6 + sec + sbc originalY + sta oamStaging,y +@notMirrorVert: inc oamStagingLength iny jsr tileModifierForCurrentPiece ; used to just load from orientationTable @@ -107,7 +147,7 @@ stageSpriteForCurrentPiece_actual: inc oamStagingLength dey lda #$FF - sta oamStaging,y + sta oamStaging-1,y iny iny lda #$00 @@ -124,6 +164,12 @@ stageSpriteForCurrentPiece_actual: clc adc generalCounter3 sta oamStaging,y + lda mirrorHorizFlag + beq @finishLoop + lda #$08 + sec + sbc oamStaging,y + sta oamStaging,y @finishLoop: inc oamStagingLength iny @@ -135,24 +181,23 @@ stageSpriteForCurrentPiece_return: stageSpriteForNextPiece: lda hideNextPiece - bne @maybeDisplayNextPiece - + bne @ret @displayNextPiece: lda #$C8 sta spriteXOffset lda #$77 sta spriteYOffset ldx nextPiece + cpx #PIECE_SPLIT_SQUARE + bne @normal + lda #SPRITE_SPLIT_SQUARE + bne @store +@normal: lda tetriminoTypeFromOrientation,x clc - adc #$6 ; piece sprites start at index 6 - sta spriteIndexInOamContentLookup + adc #SPRITE_TPIECE +@store: + sta spriteIndex jmp loadSpriteIntoOamStaging - -@maybeDisplayNextPiece: - lda practiseType - cmp #MODE_HARDDROP - beq @displayNextPiece - lda debugFlag - bne @displayNextPiece +@ret: rts diff --git a/src/tetris.nes.cfg b/src/tetris.nes.cfg index 2b877218..e8af2697 100644 --- a/src/tetris.nes.cfg +++ b/src/tetris.nes.cfg @@ -1,7 +1,10 @@ MEMORY { ZP: start = $0000, size = $0100, type = rw, file = ""; #OAM: start = $0200, size = $0100, type = rw, file = ""; - RAM: start = $0100, size = $1F00, type = rw, file = ""; + RAM: start = $0100, size = $0580, type = rw, file = ""; + MUSIC_RAM: start = $0680, size = $0080, type = rw, file = ""; + SCORE_RAM: start = $0700, size = $0060, type = rw, file = ""; + VARS_RAM: start = $0760, size = $0080, type = rw, file = ""; HDR: start = $0000, size = $0010, type = ro, fill = yes, fillval = $00; PRG: start = $8000, size = $8000, type = ro, fill = yes, fillval = $00; CHR: start = $0000, size = $4000, type = ro, fill = yes, fillval = $00; @@ -10,6 +13,9 @@ MEMORY { SEGMENTS { ZEROPAGE: load = ZP; BSS: load = RAM, type = bss; + MUSIC_RAM: load = MUSIC_RAM, type = bss; + SCORE_RAM: load = SCORE_RAM, type = bss; + VARS_RAM: load = VARS_RAM, type = bss; HEADER: load = HDR, type = ro; CHR: load = CHR, type = ro; PRG_chunk1: load = PRG, type = ro, align = $100; diff --git a/src/util/autodetect.asm b/src/util/autodetect.asm index 424c4728..e68d7fc6 100644 --- a/src/util/autodetect.asm +++ b/src/util/autodetect.asm @@ -6,15 +6,15 @@ ; Mapper detection for Holy Mapperel ; ; Copyright 2013-2017 Damian Yerrick -; +; ; This software is provided 'as-is', without any express or implied ; warranty. In no event will the authors be held liable for any damages ; arising from the use of this software. -; +; ; Permission is granted to anyone to use this software for any purpose, ; including commercial applications, and to alter it and redistribute it ; freely, subject to the following restrictions: -; +; ; 1. The origin of this software must not be misrepresented; you must not ; claim that you wrote the original software. If you use this software ; in a product, an acknowledgment in the product documentation would be diff --git a/src/util/check_region.asm b/src/util/check_region.asm index 3c710f8e..1b47813e 100644 --- a/src/util/check_region.asm +++ b/src/util/check_region.asm @@ -40,5 +40,20 @@ checkRegion: beq @ntsc lda #1 sta palFlag + + ; check to see if custom settings are applied + ; leave alone if not vanilla + lda dasModifier + cmp #NTSC_DAS + bne @ntsc + lda arrModifier + cmp #NTSC_ARR + bne @ntsc + +; vanilla - ok to change + lda #PAL_DAS + sta dasModifier + lda #PAL_ARR + sta arrModifier @ntsc: rts diff --git a/src/util/core.asm b/src/util/core.asm index 338c170e..05f68166 100644 --- a/src/util/core.asm +++ b/src/util/core.asm @@ -1,8 +1,8 @@ clearPlayfield: + ldx #0 lda #EMPTY_TILE - ldx #$C8 @loop: - sta $0400, x + sta playfield,x dex bne @loop rts @@ -10,6 +10,7 @@ clearPlayfield: clearNametable: lda #$20 sta PPUADDR +clearNametableOffset: lda #$0 sta PPUADDR lda #EMPTY_TILE @@ -26,24 +27,29 @@ clearNametable: rts drawBlackBGPalette: + ldx renderQueuePointer lda #$3F - sta PPUADDR + sta stack,x + inx lda #$0 - sta PPUADDR - ldx #$10 -@loadPaletteLoop: + sta stack,x + inx + ldy #31 + tya + sta stack,x lda #$F - sta PPUDATA - dex - bne @loadPaletteLoop +@loadPaletteLoop: + sta stack,x + inx + dey + bpl @loadPaletteLoop + inc renderQueueLength rts resetScroll: lda #0 sta ppuScrollX - sta PPUSCROLL sta ppuScrollY - sta PPUSCROLL rts random10: @@ -60,18 +66,13 @@ updateAudioWaitForNmiAndResetOamStaging: jsr updateAudio_jmp lda #$00 sta verticalBlankingInterval - nop - checkForNmi: lda verticalBlankingInterval ; label used for crash code to determine if nmi happened here or at the previous instruction nmiLoopMidpoint: beq checkForNmi - -.if KEYBOARD = 1 ; Read Family BASIC Keyboard jsr pollKeyboard -.endif resetOAMStaging: ; Hide a sprite by moving it down offscreen, by writing any values between #$EF-#$FF here. ; Sprites are never displayed on the first line of the picture, and it is impossible to place @@ -88,158 +89,49 @@ resetOAMStaging: bne @hideY rts -updateAudioAndWaitForNmi: - jsr updateAudio_jmp - lda #$00 - sta verticalBlankingInterval - nop -@checkForNmi: - lda verticalBlankingInterval - beq @checkForNmi - rts - -updateAudioWaitForNmiAndDisablePpuRendering: - jsr updateAudioAndWaitForNmi - lda currentPpuMask - and #$E1 -_updatePpuMask: +; 7 bit 0 +; ---- ---- +; BGRs bMmG +; |||| |||| +; |||| |||+- Greyscale (0: normal color, 1: greyscale) +; |||| ||+-- 1: Show background in leftmost 8 pixels of screen, 0: Hide +; |||| |+--- 1: Show sprites in leftmost 8 pixels of screen, 0: Hide +; |||| +---- 1: Enable background rendering +; |||+------ 1: Enable sprite rendering +; ||+------- Emphasize red (green on PAL/Dendy) +; |+-------- Emphasize green (red on PAL/Dendy) +; +--------- Emphasize blue + +hideSpritesAndBackground: + lda #RENDER_IDLE + sta renderMode + lda #0 sta PPUMASK - sta currentPpuMask - rts - -updateAudioWaitForNmiAndEnablePpuRendering: - jsr updateAudioAndWaitForNmi - jsr copyCurrentScrollAndCtrlToPPU - lda currentPpuMask - ora #$1E - bne _updatePpuMask -waitForVBlankAndEnableNmi: - lda PPUSTATUS - and #$80 - bne waitForVBlankAndEnableNmi - lda currentPpuCtrl - ora #$80 - bne _updatePpuCtrl -disableNmi: - lda currentPpuCtrl - and #$7F -_updatePpuCtrl: - sta PPUCTRL - sta currentPpuCtrl rts -copyCurrentScrollAndCtrlToPPU: - lda ppuScrollX - sta PPUSCROLL - lda ppuScrollY - sta PPUSCROLL - lda currentPpuCtrl - sta PPUCTRL - rts - -bulkCopyToPpu: - jsr copyAddrAtReturnAddressToTmp_incrReturnAddrBy2 - jmp copyToPpu - -LAA9E: pha - sta PPUADDR - iny - lda (tmp1),y - sta PPUADDR - iny - lda (tmp1),y - asl a +showSpriteAndBackground: + lda renderMode pha - lda currentPpuCtrl - ora #$04 - bcs LAAB5 - and #$FB -LAAB5: sta PPUCTRL - sta currentPpuCtrl - pla - asl a - php - bcc LAAC2 - ora #$02 - iny -LAAC2: plp - clc - bne LAAC7 - sec -LAAC7: ror a - lsr a - tax -LAACA: bcs LAACD - iny -LAACD: lda (tmp1),y - sta PPUDATA - dex - bne LAACA + lda #RENDER_IDLE + sta renderMode + jsr waitForNmi + lda #%00011110 + sta PPUMASK pla - cmp #$3F - bne LAAE6 - sta PPUADDR - stx PPUADDR - stx PPUADDR - stx PPUADDR -LAAE6: sec - tya - adc tmp1 - sta tmp1 - lda #$00 - adc tmp2 - sta tmp2 -; Address to read from stored in tmp1/2 -copyToPpu: - ldx PPUSTATUS - ldy #$00 - lda (tmp1),y - bpl LAAFC + sta renderMode rts -LAAFC: cmp #$60 - bne LAB0A - pla - sta tmp2 - pla - sta tmp1 - ldy #$02 - bne LAAE6 -LAB0A: cmp #$4C - bne LAA9E - lda tmp1 - pha - lda tmp2 - pha - iny - lda (tmp1),y - tax - iny - lda (tmp1),y - sta tmp2 - stx tmp1 - bcs copyToPpu -copyAddrAtReturnAddressToTmp_incrReturnAddrBy2: - tsx - lda stack+3,x - sta tmpBulkCopyToPpuReturnAddr - lda stack+4,x - sta tmpBulkCopyToPpuReturnAddr+1 - ldy #$01 - lda (tmpBulkCopyToPpuReturnAddr),y - sta tmp1 - iny - lda (tmpBulkCopyToPpuReturnAddr),y - sta tmp2 - clc - lda #$02 - adc tmpBulkCopyToPpuReturnAddr - sta stack+3,x +updateAudioAndWaitForNmi: + jsr updateAudio_jmp +waitForNmi: lda #$00 - adc tmpBulkCopyToPpuReturnAddr+1 - sta stack+4,x + sta verticalBlankingInterval +@checkForNmi: + lda verticalBlankingInterval + beq @checkForNmi rts + ;reg x: zeropage addr of seed generateNextPseudorandomNumber5x: jsr generateNextPseudorandomNumber @@ -264,31 +156,39 @@ 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 - txa - sty tmp2 - clc - sbc tmp2 - tax - pla - ldy #$00 - sty tmp1 -@setByte: - sta (tmp1),y - dey - bne @setByte - dec tmp2 - inx - bne @setByte - rts +copyPatchAtXYToQueue: + stx patchPtr + sty patchPtr+1 +@counter = generalCounter + ldx renderQueuePointer + ldy #0 +@stripe: +; high ppu byte or end marker + lda (patchPtr),y + beq @end + sta stack,x + iny + inx +; low ppu byte + lda (patchPtr),y + sta stack,x + iny + inx +; length + lda (patchPtr),y + sta stack,x + sta @counter + inx + iny +@tile: + lda (patchPtr),y + sta stack,x + inx + iny + dec @counter + bpl @tile + inc renderQueueLength + bne @stripe +@end: + stx renderQueuePointer + rts diff --git a/src/util/mapper.asm b/src/util/mapper.asm index 7abd1949..db3bb141 100644 --- a/src/util/mapper.asm +++ b/src/util/mapper.asm @@ -44,12 +44,12 @@ _setMMC1Control: changeCHRBanks: ; accum should be 0 or 2 (CHRBankset0 or CHRBankset1) - sta generalCounter + sta generalCounter ; autodetect .if INES_MAPPER = 1000 - ldx mapperId - beq @cnrom + ldx mapperId + beq @cnrom changeCHRBanksMMC1 rts @cnrom: diff --git a/src/util/modetext.asm b/src/util/modetext.asm index bad788b6..b0e41676 100644 --- a/src/util/modetext.asm +++ b/src/util/modetext.asm @@ -1,44 +1,211 @@ displayModeText: - ldx practiseType - cpx #MODE_SEED - bne @drawModeName - ; draw seed instead - lda tmp1 - sta PPUADDR - lda tmp2 - sta PPUADDR - lda set_seed_input - jsr twoDigsToPPU - lda set_seed_input+1 - jsr twoDigsToPPU - lda set_seed_input+2 - jsr twoDigsToPPU - rts + lda #$00 + sta anydasFlag +; set anydasFlag + lda disableDasFlag + bne @anydas + lda noWallChargeFlag + bne @anydas + lda entryChargeModifier + bne @anydas + lda palFlag + bne @pal -@drawModeName: - ; ldx practiseType - lda #0 -@loopAddr: - cpx #0 - beq @addr +; set regional differences + ldx #NTSC_DAS + ldy #NTSC_ARR + bne @testAnydas +@pal: + ldx #PAL_DAS + ldy #PAL_ARR + +; test das & arr values +@testAnydas: + cpx dasModifier + bne @anydas + + cpy arrModifier + beq @notanydas +@anydas: + jsr @notanydas + lda gameMode + cmp #3 + bne @notMenu + stagePatch menuAnydasPatch + jmp render_mode_queue +@notMenu: + lda gameModeState + bne @ret + stagePatch gameAnydasPatch + jmp render_mode_queue + +@notanydas: + lda practiseType + asl + sta generalCounter + asl clc - adc #6 - dex - jmp @loopAddr -@addr: - ; offset in X + adc generalCounter tax - +@drawMode: lda tmp1 sta PPUADDR lda tmp2 sta PPUADDR - +@startLoop: ldy #6 @writeChar: - lda modeText, x + lda modeText-6, x sta PPUDATA inx dey bne @writeChar + +; cover TYPE with seed if seeded b type + lda practiseType + cmp #MODE_TYPEB + bne @ret + lda typeBSeedFlag + beq @ret + lda tmp1 + sta PPUADDR + lda tmp2 + clc + adc #2 + sta PPUADDR + lda b_seed_input + jsr twoDigsToPPU + lda b_seed_input+1 + jsr twoDigsToPPU +@ret: rts + +patchSeed: + ; skip if not seeded + lda seedEnabled + beq @menuOnlyItems + lda seededPieces + beq @menuOnlyItems + sty PPUADDR + stx PPUADDR + + lda set_seed_input + jsr twoDigsToPPU + lda set_seed_input+1 + jsr twoDigsToPPU + lda set_seed_input+2 + jsr twoDigsToPPU + lda gameMode + cmp #3 + bne @notMenu + stagePatch menuSeedPatch + jsr render_mode_queue + jmp @menuOnlyItems +@notMenu: + lda gameModeState + bne @notVits + stagePatch gameSeedPatch + jmp render_mode_queue +@menuOnlyItems: + + lda gameMode + cmp #3 + bne @notVits + + jsr drawCrashMode + + lda transFlag + beq @notTrans + stagePatch menuTransPatch +@notTrans: + lda sxtoklFlag + beq @notSxtokl + stagePatch menuSxtoklPatch +@notSxtokl: + lda teppozFlag + beq @notTeppoz + stagePatch menuTeppozPatch +@notTeppoz: + lda palpepFlag + beq @notPalpep + stagePatch menuPalpepPatch +@notPalpep: + lda dasOnlyFlag + beq @notDasOnly + stagePatch menuDasOnlyPatch +@notDasOnly: + lda vitsScoreFlag + beq @notVits + stagePatch menuVitsPatch +@notVits: + jmp render_mode_queue + + +; this and the linecap display can be combined, with the string lists from menudata.asm used +crashStrings: + .word STR_SHOW + .word STR_TOP + .word STR_CRASH +drawCrashMode: + lda crashModifier + beq @ret + ldy #$21 + sty PPUADDR + ldy #$35 + sty PPUADDR + asl + tay + ; use offset, crashModifier will be 1,2 or 3, never 0 + ldx crashStrings-1,y + lda crashStrings-2,y + tay + jsr stringBackgroundXY + ldy strictFlag + beq @ret + ldy #$FF + sty PPUDATA + ldy #'S' + sty PPUDATA +@ret: + rts + +menuSeedPatch: + .byte $20,$B5,$0,$3B + .byte $20,$BC,$0,$3C + .byte $20,$D5,$7,$3D,$3E,$3E,$3E,$3E,$3E,$3E,$3F + .byte $0 + +gameSeedPatch: + .byte $20,$A2,$0,$35 + .byte $20,$A9,$0,$36 + .byte $20,$C2,$7,$76,$37,$37,$37,$37,$37,$37,$77 + .byte $0 + +menuAnydasPatch: + .byte $20,$55,$7,$38,$39,$39,$39,$39,$39,$39,$3A + .byte $20,$75,$7,$3B,"A","N","Y","D","A","S",$3C + .byte $0 + +gameAnydasPatch: + .byte $20,$42,$7,$74,$34,$34,$34,$34,$34,$34,$75 + .byte $20,$62,$7,$35,"A","N","Y","D","A","S",$36 + .byte $0 + +menuSxtoklPatch: + .byte $20,$63,$5,"SXTOKL" + .byte $0 +menuPalpepPatch: + .byte $20,$83,$5,"PALPEP" + .byte $0 +menuTeppozPatch: + .byte $20,$A3,$5,"TEPPOZ" + .byte $0 +menuDasOnlyPatch: + .byte $20,$C3,$7,"DAS",$FF,"ONLY" + .byte $0 +menuTransPatch: + .byte $20,$E3,$4,"TRANS" + .byte $0 +menuVitsPatch: + .byte $21,$03,$3,"VITS" + .byte $0 diff --git a/src/util/strings.asm b/src/util/strings.asm index f5ab17ee..323149bc 100644 --- a/src/util/strings.asm +++ b/src/util/strings.asm @@ -1,167 +1,53 @@ -stringBackground: - ldx stringIndexLookup - lda stringLookup, x - tax - lda stringLookup, x - sta tmpZ - inx +stringUnpackXY: + txa + lsr + lsr + lsr + lsr + sta stringLength + tya + clc + adc #strTable + sta tmp2 ldy #0 -@loop: - lda stringLookup, x - sta PPUDATA - inx - iny - cpy tmpZ - bne @loop rts -stringSprite: - ldx spriteIndexInOamContentLookup - lda stringLookup, x - tax - lda stringLookup, x - sta tmpZ - inx - lda spriteXOffset - sta tmpX - jmp stringSpriteLoop - -stringSpriteAlignRight: - ldx spriteIndexInOamContentLookup - lda stringLookup, x - tax - lda stringLookup, x - inx - sta tmpZ - lda tmpZ - asl - asl - asl - sta tmpX - clc - lda spriteXOffset - sbc tmpX - sta tmpX - -stringSpriteLoop: - ldy oamStagingLength - sec +stringSpriteXY: + jsr stringUnpackXY + ldx oamStagingLength +@loop: lda spriteYOffset - sta oamStaging, y - lda stringLookup, x - inx - sta oamStaging+1, y - lda #$00 - sta oamStaging+2, y - lda tmpX - sta oamStaging+3, y + sta oamStaging,x + lda (tmp1),y + sta oamStaging+1,x + lda stringAttrib + sta oamStaging+2,x + lda spriteXOffset + sta oamStaging+3,x clc adc #$8 - sta tmpX + sta spriteXOffset ; increase OAM index - lda #$04 - clc - adc oamStagingLength - sta oamStagingLength - - dec tmpZ - lda tmpZ - bne stringSpriteLoop + inx + inx + inx + inx + stx oamStagingLength + iny + dec stringLength + bpl @loop 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' +stringBackgroundXY: + jsr stringUnpackXY +@loop: + lda (tmp1),y + sta PPUDATA + iny + dec stringLength + bpl @loop + rts diff --git a/tests/src/garbage.rs b/tests/src/garbage.rs index 6da54e0d..361caf1e 100644 --- a/tests/src/garbage.rs +++ b/tests/src/garbage.rs @@ -15,7 +15,7 @@ pub fn test_garbage4_crash() { let wait_loop_end: u16 = labels::get("resetOAMStaging") as u16; // spend a few frames bootstrapping - for _ in 0..3 { + for _ in 0..4 { emu.run_until_vblank(); } diff --git a/tests/src/mapper.rs b/tests/src/mapper.rs old mode 100755 new mode 100644 index 066a1d51..bdeb15fb --- a/tests/src/mapper.rs +++ b/tests/src/mapper.rs @@ -3,6 +3,7 @@ use crate::{ labels, playfield, util, + // video, }; use rustico_core::nes::NesState; @@ -69,7 +70,7 @@ pub fn test_tilesets(rom: Option<&[u8]>) { let tile_select = get_tile_select(&mut emu); let current_tileset = get_current_tilesets(&mut emu); assert_eq!(tile_select, 0); - assert_eq!(current_tileset, tileset1); + assert_eq!(current_tileset, tileset2); // test game mode let practise_type = labels::get("practiseType") as usize; diff --git a/tests/src/nmi.rs b/tests/src/nmi.rs index eca1d6f1..ddb1c326 100644 --- a/tests/src/nmi.rs +++ b/tests/src/nmi.rs @@ -12,7 +12,7 @@ pub fn test() { let render_flags = labels::get("renderFlags") as usize; // spend a few frames bootstrapping - for _ in 0..3 { + for _ in 0..4 { emu.run_until_vblank(); } @@ -38,21 +38,21 @@ pub fn test() { ##### ### ###### ### ###### ### -######### -######### -######### -######### -######### -######### -######### -######### -######### -######### -######### -######### -######### -######### -######### +######### +######### +######### +######### +######### +######### +######### +######### +######### +######### +######### +######### +######### +######### +######### ######### "##); for _ in 0..50 { diff --git a/tests/src/palettes.rs b/tests/src/palettes.rs index 969bbcfe..34dedf8d 100644 --- a/tests/src/palettes.rs +++ b/tests/src/palettes.rs @@ -269,7 +269,7 @@ pub fn test() { // spend a few frames bootstrapping - for _ in 0..3 { + for _ in 0..4 { emu.run_until_vblank(); } diff --git a/tests/src/toprow.rs b/tests/src/toprow.rs index dd79ec3b..65b2474d 100644 --- a/tests/src/toprow.rs +++ b/tests/src/toprow.rs @@ -3,7 +3,7 @@ use crate::{util, labels, playfield}; pub fn test() { let mut emu = util::emulator(None); - for _ in 0..3 { emu.run_until_vblank(); } + for _ in 0..5 { emu.run_until_vblank(); } let game_mode = labels::get("gameMode") as usize; let main_loop = labels::get("mainLoop"); diff --git a/tests/src/tspins.rs b/tests/src/tspins.rs index 6029f11e..098d2346 100644 --- a/tests/src/tspins.rs +++ b/tests/src/tspins.rs @@ -3,7 +3,7 @@ use crate::{util, labels, playfield}; pub fn test() { let mut emu = util::emulator(None); - for _ in 0..4 { + for _ in 0..6 { emu.run_until_vblank(); } diff --git a/tools/assemble/ca65.js b/tools/assemble/ca65.js index b3a04380..f40892dd 100644 --- a/tools/assemble/ca65.js +++ b/tools/assemble/ca65.js @@ -1,8 +1,52 @@ // include: shell.js +// include: minimum_runtime_check.js +(function() { + // "30.0.0" -> 300000 + function humanReadableVersionToPacked(str) { + str = str.split('-')[0]; // Remove any trailing part from e.g. "12.53.3-alpha" + var vers = str.split('.').slice(0, 3); + while(vers.length < 3) vers.push('00'); + vers = vers.map((n, i, arr) => n.padStart(2, '0')); + return vers.join(''); + } + // 300000 -> "30.0.0" + var packedVersionToHumanReadable = n => [n / 10000 | 0, (n / 100 | 0) % 100, n % 100].join('.'); + + var TARGET_NOT_SUPPORTED = 2147483647; + + // Note: We use a typeof check here instead of optional chaining using + // globalThis because older browsers might not have globalThis defined. + var currentNodeVersion = typeof process !== 'undefined' && process.versions?.node ? humanReadableVersionToPacked(process.versions.node) : TARGET_NOT_SUPPORTED; + if (currentNodeVersion < 180300) { + throw new Error(`This emscripten-generated code requires node v${ packedVersionToHumanReadable(180300) } (detected v${packedVersionToHumanReadable(currentNodeVersion)})`); + } + + var userAgent = typeof navigator !== 'undefined' && navigator.userAgent; + if (!userAgent) { + return; + } + + var currentSafariVersion = userAgent.includes("Safari/") && !userAgent.includes("Chrome/") && userAgent.match(/Version\/(\d+\.?\d*\.?\d*)/) ? humanReadableVersionToPacked(userAgent.match(/Version\/(\d+\.?\d*\.?\d*)/)[1]) : TARGET_NOT_SUPPORTED; + if (currentSafariVersion < 150000) { + throw new Error(`This emscripten-generated code requires Safari v${ packedVersionToHumanReadable(150000) } (detected v${currentSafariVersion})`); + } + + var currentFirefoxVersion = userAgent.match(/Firefox\/(\d+(?:\.\d+)?)/) ? parseFloat(userAgent.match(/Firefox\/(\d+(?:\.\d+)?)/)[1]) : TARGET_NOT_SUPPORTED; + if (currentFirefoxVersion < 79) { + throw new Error(`This emscripten-generated code requires Firefox v79 (detected v${currentFirefoxVersion})`); + } + + var currentChromeVersion = userAgent.match(/Chrome\/(\d+(?:\.\d+)?)/) ? parseFloat(userAgent.match(/Chrome\/(\d+(?:\.\d+)?)/)[1]) : TARGET_NOT_SUPPORTED; + if (currentChromeVersion < 85) { + throw new Error(`This emscripten-generated code requires Chrome v85 (detected v${currentChromeVersion})`); + } +})(); + +// end include: minimum_runtime_check.js // The Module object: Our interface to the outside world. We import // and export values on it. There are various ways Module can be used: // 1. Not defined. We create it here -// 2. A function parameter, function(Module) { ..generated code.. } +// 2. A function parameter, function(moduleArg) => Promise // 3. pre-run appended it, var Module = {}; ..generated code.. // 4. External script tag defines var Module. // We need to check if Module already exists (e.g. case 3 above). @@ -14,36 +58,36 @@ // can continue to use Module afterwards as well. var Module = typeof Module != 'undefined' ? Module : {}; +// Determine the runtime environment we are in. You can customize this by +// setting the ENVIRONMENT setting at compile time (see settings.js). + +// Attempt to auto-detect the environment +var ENVIRONMENT_IS_WEB = !!globalThis.window; +var ENVIRONMENT_IS_WORKER = !!globalThis.WorkerGlobalScope; +// N.b. Electron.js environment is simultaneously a NODE-environment, but +// also a web environment. +var ENVIRONMENT_IS_NODE = globalThis.process?.versions?.node && globalThis.process?.type != 'renderer'; +var ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER; + // --pre-jses are emitted after the Module integration code, so that they can // refer to Module (if they choose; they can also define Module) -// Sometimes an existing Module object exists with properties -// meant to overwrite the default module functionality. Here -// we collect those properties and reapply _after_ we configure -// the current environment's defaults to avoid having to be so -// defensive during initialization. -var moduleOverrides = Object.assign({}, Module); - var arguments_ = []; var thisProgram = './this.program'; var quit_ = (status, toThrow) => { throw toThrow; }; -// Determine the runtime environment we are in. You can customize this by -// setting the ENVIRONMENT setting at compile time (see settings.js). - -// Attempt to auto-detect the environment -var ENVIRONMENT_IS_WEB = typeof window == 'object'; -var ENVIRONMENT_IS_WORKER = typeof importScripts == 'function'; -// N.b. Electron.js environment is simultaneously a NODE-environment, but -// also a web environment. -var ENVIRONMENT_IS_NODE = typeof process == 'object' && typeof process.versions == 'object' && typeof process.versions.node == 'string'; -var ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER; +// In MODULARIZE mode _scriptName needs to be captured already at the very top of the page immediately when the page is parsed, so it is generated there +// before the page load. In non-MODULARIZE modes generate it here. +var _scriptName = globalThis.document?.currentScript?.src; -if (Module['ENVIRONMENT']) { - throw new Error('Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -sENVIRONMENT=web or -sENVIRONMENT=node)'); +if (typeof __filename != 'undefined') { // Node + _scriptName = __filename; +} else +if (ENVIRONMENT_IS_WORKER) { + _scriptName = self.location.href; } // `/` should be present at the end if `scriptDirectory` is not empty @@ -56,192 +100,72 @@ function locateFile(path) { } // Hooks that are implemented differently in different runtime environments. -var read_, - readAsync, - readBinary; +var readAsync, readBinary; if (ENVIRONMENT_IS_NODE) { - if (typeof process == 'undefined' || !process.release || process.release.name !== 'node') throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); - - var nodeVersion = process.versions.node; - var numericVersion = nodeVersion.split('.').slice(0, 3); - numericVersion = (numericVersion[0] * 10000) + (numericVersion[1] * 100) + (numericVersion[2].split('-')[0] * 1); - var minVersion = 160000; - if (numericVersion < 160000) { - throw new Error('This emscripten-generated code requires node v16.0.0 (detected v' + nodeVersion + ')'); - } + const isNode = globalThis.process?.versions?.node && globalThis.process?.type != 'renderer'; + if (!isNode) throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); - // `require()` is no-op in an ESM module, use `createRequire()` to construct - // the require()` function. This is only necessary for multi-environment - // builds, `-sENVIRONMENT=node` emits a static import declaration instead. - // TODO: Swap all `require()`'s with `import()`'s? // These modules will usually be used on Node.js. Load them eagerly to avoid // the complexity of lazy-loading. - var fs = require('fs'); - var nodePath = require('path'); + var fs = require('node:fs'); - if (ENVIRONMENT_IS_WORKER) { - scriptDirectory = nodePath.dirname(scriptDirectory) + '/'; - } else { - scriptDirectory = __dirname + '/'; - } + scriptDirectory = __dirname + '/'; // include: node_shell_read.js -read_ = (filename, binary) => { - // We need to re-wrap `file://` strings to URLs. Normalizing isn't - // necessary in that case, the path should already be absolute. - filename = isFileURI(filename) ? new URL(filename) : nodePath.normalize(filename); - return fs.readFileSync(filename, binary ? undefined : 'utf8'); -}; - readBinary = (filename) => { - var ret = read_(filename, true); - if (!ret.buffer) { - ret = new Uint8Array(ret); - } - assert(ret.buffer); + // We need to re-wrap `file://` strings to URLs. + filename = isFileURI(filename) ? new URL(filename) : filename; + var ret = fs.readFileSync(filename); + assert(Buffer.isBuffer(ret)); return ret; }; -readAsync = (filename, onload, onerror, binary = true) => { - // See the comment in the `read_` function. - filename = isFileURI(filename) ? new URL(filename) : nodePath.normalize(filename); - fs.readFile(filename, binary ? undefined : 'utf8', (err, data) => { - if (err) onerror(err); - else onload(binary ? data.buffer : data); - }); +readAsync = async (filename, binary = true) => { + // See the comment in the `readBinary` function. + filename = isFileURI(filename) ? new URL(filename) : filename; + var ret = fs.readFileSync(filename, binary ? undefined : 'utf8'); + assert(binary ? Buffer.isBuffer(ret) : typeof ret == 'string'); + return ret; }; // end include: node_shell_read.js - if (!Module['thisProgram'] && process.argv.length > 1) { + if (process.argv.length > 1) { thisProgram = process.argv[1].replace(/\\/g, '/'); } arguments_ = process.argv.slice(2); + // MODULARIZE will export the module in the proper place outside, we don't need to export here if (typeof module != 'undefined') { module['exports'] = Module; } - process.on('uncaughtException', (ex) => { - // suppress ExitStatus exceptions from showing an error - if (ex !== 'unwind' && !(ex instanceof ExitStatus) && !(ex.context instanceof ExitStatus)) { - throw ex; - } - }); - quit_ = (status, toThrow) => { process.exitCode = status; throw toThrow; }; - Module['inspect'] = () => '[Emscripten Module object]'; - } else if (ENVIRONMENT_IS_SHELL) { - if ((typeof process == 'object' && typeof require === 'function') || typeof window == 'object' || typeof importScripts == 'function') throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); - - if (typeof read != 'undefined') { - read_ = read; - } - - readBinary = (f) => { - if (typeof readbuffer == 'function') { - return new Uint8Array(readbuffer(f)); - } - let data = read(f, 'binary'); - assert(typeof data == 'object'); - return data; - }; - - readAsync = (f, onload, onerror) => { - setTimeout(() => onload(readBinary(f))); - }; - - if (typeof clearTimeout == 'undefined') { - globalThis.clearTimeout = (id) => {}; - } - - if (typeof setTimeout == 'undefined') { - // spidermonkey lacks setTimeout but we use it above in readAsync. - globalThis.setTimeout = (f) => (typeof f == 'function') ? f() : abort(); - } - - if (typeof scriptArgs != 'undefined') { - arguments_ = scriptArgs; - } else if (typeof arguments != 'undefined') { - arguments_ = arguments; - } - - if (typeof quit == 'function') { - quit_ = (status, toThrow) => { - // Unlike node which has process.exitCode, d8 has no such mechanism. So we - // have no way to set the exit code and then let the program exit with - // that code when it naturally stops running (say, when all setTimeouts - // have completed). For that reason, we must call `quit` - the only way to - // set the exit code - but quit also halts immediately. To increase - // consistency with node (and the web) we schedule the actual quit call - // using a setTimeout to give the current stack and any exception handlers - // a chance to run. This enables features such as addOnPostRun (which - // expected to be able to run code after main returns). - setTimeout(() => { - if (!(toThrow instanceof ExitStatus)) { - let toLog = toThrow; - if (toThrow && typeof toThrow == 'object' && toThrow.stack) { - toLog = [toThrow, toThrow.stack]; - } - err(`exiting due to exception: ${toLog}`); - } - quit(status); - }); - throw toThrow; - }; - } - - if (typeof print != 'undefined') { - // Prefer to use print/printErr where they exist, as they usually work better. - if (typeof console == 'undefined') console = /** @type{!Console} */({}); - console.log = /** @type{!function(this:Console, ...*): undefined} */ (print); - console.warn = console.error = /** @type{!function(this:Console, ...*): undefined} */ (typeof printErr != 'undefined' ? printErr : print); - } - } else // Note that this includes Node.js workers when relevant (pthreads is enabled). // Node.js workers are detected as a combination of ENVIRONMENT_IS_WORKER and // ENVIRONMENT_IS_NODE. if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { - if (ENVIRONMENT_IS_WORKER) { // Check worker, not web, since window could be polyfilled - scriptDirectory = self.location.href; - } else if (typeof document != 'undefined' && document.currentScript) { // web - scriptDirectory = document.currentScript.src; - } - // blob urls look like blob:http://site.com/etc/etc and we cannot infer anything from them. - // otherwise, slice off the final part of the url to find the script directory. - // if scriptDirectory does not contain a slash, lastIndexOf will return -1, - // and scriptDirectory will correctly be replaced with an empty string. - // If scriptDirectory contains a query (starting with ?) or a fragment (starting with #), - // they are removed because they could contain a slash. - if (scriptDirectory.indexOf('blob:') !== 0) { - scriptDirectory = scriptDirectory.substr(0, scriptDirectory.replace(/[?#].*/, "").lastIndexOf('/')+1); - } else { - scriptDirectory = ''; + try { + scriptDirectory = new URL('.', _scriptName).href; // includes trailing slash + } catch { + // Must be a `blob:` or `data:` URL (e.g. `blob:http://site.com/etc/etc`), we cannot + // infer anything from them. } - if (!(typeof window == 'object' || typeof importScripts == 'function')) throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); + if (!(globalThis.window || globalThis.WorkerGlobalScope)) throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); - // Differentiate the Web Worker from the Node Worker case, as reading must - // be done differently. { // include: web_or_worker_shell_read.js -read_ = (url) => { - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, false); - xhr.send(null); - return xhr.responseText; - } - - if (ENVIRONMENT_IS_WORKER) { +if (ENVIRONMENT_IS_WORKER) { readBinary = (url) => { var xhr = new XMLHttpRequest(); xhr.open('GET', url, false); @@ -251,21 +175,33 @@ read_ = (url) => { }; } - readAsync = (url, onload, onerror) => { - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, true); - xhr.responseType = 'arraybuffer'; - xhr.onload = () => { - if (xhr.status == 200 || (xhr.status == 0 && xhr.response)) { // file URLs can return 0 - onload(xhr.response); - return; - } - onerror(); - }; - xhr.onerror = onerror; - xhr.send(null); - } - + readAsync = async (url) => { + // Fetch has some additional restrictions over XHR, like it can't be used on a file:// url. + // See https://github.com/github/fetch/pull/92#issuecomment-140665932 + // Cordova or Electron apps are typically loaded from a file:// url. + // So use XHR on webview if URL is a file URL. + if (isFileURI(url)) { + return new Promise((resolve, reject) => { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = 'arraybuffer'; + xhr.onload = () => { + if (xhr.status == 200 || (xhr.status == 0 && xhr.response)) { // file URLs can return 0 + resolve(xhr.response); + return; + } + reject(xhr.status); + }; + xhr.onerror = reject; + xhr.send(null); + }); + } + var response = await fetch(url, { credentials: 'same-origin' }); + if (response.ok) { + return response.arrayBuffer(); + } + throw new Error(response.status + ' : ' + response.url); + }; // end include: web_or_worker_shell_read.js } } else @@ -273,43 +209,9 @@ read_ = (url) => { throw new Error('environment detection error'); } -var out = Module['print'] || console.log.bind(console); -var err = Module['printErr'] || console.error.bind(console); - -// Merge back in the overrides -Object.assign(Module, moduleOverrides); -// Free the object hierarchy contained in the overrides, this lets the GC -// reclaim data used e.g. in memoryInitializerRequest, which is a large typed array. -moduleOverrides = null; -checkIncomingModuleAPI(); - -// Emit code to handle expected values on the Module object. This applies Module.x -// to the proper local x. This has two benefits: first, we only emit it if it is -// expected to arrive, and second, by using a local everywhere else that can be -// minified. - -if (Module['arguments']) arguments_ = Module['arguments'];legacyModuleProp('arguments', 'arguments_'); - -if (Module['thisProgram']) thisProgram = Module['thisProgram'];legacyModuleProp('thisProgram', 'thisProgram'); - -if (Module['quit']) quit_ = Module['quit'];legacyModuleProp('quit', 'quit_'); - -// perform assertions in shell.js after we set up out() and err(), as otherwise if an assertion fails it cannot print the message -// Assertions on removed incoming Module JS APIs. -assert(typeof Module['memoryInitializerPrefixURL'] == 'undefined', 'Module.memoryInitializerPrefixURL option was removed, use Module.locateFile instead'); -assert(typeof Module['pthreadMainPrefixURL'] == 'undefined', 'Module.pthreadMainPrefixURL option was removed, use Module.locateFile instead'); -assert(typeof Module['cdInitializerPrefixURL'] == 'undefined', 'Module.cdInitializerPrefixURL option was removed, use Module.locateFile instead'); -assert(typeof Module['filePackagePrefixURL'] == 'undefined', 'Module.filePackagePrefixURL option was removed, use Module.locateFile instead'); -assert(typeof Module['read'] == 'undefined', 'Module.read option was removed (modify read_ in JS)'); -assert(typeof Module['readAsync'] == 'undefined', 'Module.readAsync option was removed (modify readAsync in JS)'); -assert(typeof Module['readBinary'] == 'undefined', 'Module.readBinary option was removed (modify readBinary in JS)'); -assert(typeof Module['setWindowTitle'] == 'undefined', 'Module.setWindowTitle option was removed (modify emscripten_set_window_title in JS)'); -assert(typeof Module['TOTAL_MEMORY'] == 'undefined', 'Module.TOTAL_MEMORY has been renamed Module.INITIAL_MEMORY'); -legacyModuleProp('asm', 'wasmExports'); -legacyModuleProp('read', 'read_'); -legacyModuleProp('readAsync', 'readAsync'); -legacyModuleProp('readBinary', 'readBinary'); -legacyModuleProp('setWindowTitle', 'setWindowTitle'); +var out = console.log.bind(console); +var err = console.error.bind(console); + var IDBFS = 'IDBFS is no longer included by default; build with -lidbfs.js'; var PROXYFS = 'PROXYFS is no longer included by default; build with -lproxyfs.js'; var WORKERFS = 'WORKERFS is no longer included by default; build with -lworkerfs.js'; @@ -320,10 +222,13 @@ var OPFS = 'OPFS is no longer included by default; build with -lopfs.js'; -assert(!ENVIRONMENT_IS_SHELL, "shell environment detected but not enabled at build time. Add 'shell' to `-sENVIRONMENT` to enable."); +// perform assertions in shell.js after we set up out() and err(), as otherwise +// if an assertion fails it cannot print the message +assert(!ENVIRONMENT_IS_SHELL, 'shell environment detected but not enabled at build time (add `shell` to `-sENVIRONMENT` to enable)'); // end include: shell.js + // include: preamble.js // === Preamble library stuff === @@ -336,42 +241,13 @@ assert(!ENVIRONMENT_IS_SHELL, "shell environment detected but not enabled at bui // is up at http://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html var wasmBinary; -if (Module['wasmBinary']) wasmBinary = Module['wasmBinary'];legacyModuleProp('wasmBinary', 'wasmBinary'); - -if (typeof WebAssembly != 'object') { - abort('no native wasm support detected'); -} - -// include: base64Utils.js -// Converts a string of base64 into a byte array (Uint8Array). -function intArrayFromBase64(s) { - if (typeof ENVIRONMENT_IS_NODE != 'undefined' && ENVIRONMENT_IS_NODE) { - var buf = Buffer.from(s, 'base64'); - return new Uint8Array(buf.buffer, buf.byteOffset, buf.length); - } - var decoded = atob(s); - var bytes = new Uint8Array(decoded.length); - for (var i = 0 ; i < decoded.length ; ++i) { - bytes[i] = decoded.charCodeAt(i); - } - return bytes; +if (!globalThis.WebAssembly) { + err('no native wasm support detected'); } -// If filename is a base64 data URI, parses and returns data (Buffer on node, -// Uint8Array otherwise). If filename is not a base64 data URI, returns undefined. -function tryParseAsDataURI(filename) { - if (!isDataURI(filename)) { - return; - } - - return intArrayFromBase64(filename.slice(dataURIPrefix.length)); -} -// end include: base64Utils.js // Wasm globals -var wasmMemory; - //======================================== // Runtime essentials //======================================== @@ -381,7 +257,7 @@ var wasmMemory; var ABORT = false; // set by exit() and abort(). Passed to 'onExit' handler. -// NOTE: This is also used as the process return code code in shell environments +// NOTE: This is also used as the process return code in shell environments // but only when noExitRuntime is false. var EXITSTATUS; @@ -398,48 +274,21 @@ function assert(condition, text) { // We used to include malloc/free by default in the past. Show a helpful error in // builds with assertions. - -// Memory management - -var HEAP, -/** @type {!Int8Array} */ - HEAP8, -/** @type {!Uint8Array} */ - HEAPU8, -/** @type {!Int16Array} */ - HEAP16, -/** @type {!Uint16Array} */ - HEAPU16, -/** @type {!Int32Array} */ - HEAP32, -/** @type {!Uint32Array} */ - HEAPU32, -/** @type {!Float32Array} */ - HEAPF32, -/** @type {!Float64Array} */ - HEAPF64; - -function updateMemoryViews() { - var b = wasmMemory.buffer; - Module['HEAP8'] = HEAP8 = new Int8Array(b); - Module['HEAP16'] = HEAP16 = new Int16Array(b); - Module['HEAPU8'] = HEAPU8 = new Uint8Array(b); - Module['HEAPU16'] = HEAPU16 = new Uint16Array(b); - Module['HEAP32'] = HEAP32 = new Int32Array(b); - Module['HEAPU32'] = HEAPU32 = new Uint32Array(b); - Module['HEAPF32'] = HEAPF32 = new Float32Array(b); - Module['HEAPF64'] = HEAPF64 = new Float64Array(b); +function _malloc() { + abort('malloc() called but not included in the build - add `_malloc` to EXPORTED_FUNCTIONS'); +} +function _free() { + // Show a helpful error since we used to include free by default in the past. + abort('free() called but not included in the build - add `_free` to EXPORTED_FUNCTIONS'); } -assert(!Module['STACK_SIZE'], 'STACK_SIZE can no longer be set at runtime. Use -sSTACK_SIZE at link time') - -assert(typeof Int32Array != 'undefined' && typeof Float64Array !== 'undefined' && Int32Array.prototype.subarray != undefined && Int32Array.prototype.set != undefined, - 'JS engine does not provide full typed array support'); - -// If memory is defined in wasm, the user can't provide it, or set INITIAL_MEMORY -assert(!Module['wasmMemory'], 'Use of `wasmMemory` detected. Use -sIMPORTED_MEMORY to define wasmMemory externally'); -assert(!Module['INITIAL_MEMORY'], 'Detected runtime INITIAL_MEMORY setting. Use -sIMPORTED_MEMORY to define wasmMemory dynamically'); +/** + * Indicates whether filename is delivered via file protocol (as opposed to http/https) + * @noinline + */ +var isFileURI = (filename) => filename.startsWith('file://'); +// include: runtime_common.js // include: runtime_stack_check.js // Initializes the stack cookie. Called at the startup of main and at the startup of each thread in pthreads mode. function writeStackCookie() { @@ -478,24 +327,164 @@ function checkStackCookie() { } } // end include: runtime_stack_check.js -// include: runtime_assertions.js +// include: runtime_exceptions.js +// Base Emscripten EH error class +class EmscriptenEH {} + +class EmscriptenSjLj extends EmscriptenEH {} + +// end include: runtime_exceptions.js +// include: runtime_debug.js +var runtimeDebug = true; // Switch to false at runtime to disable logging at the right times + +// Used by XXXXX_DEBUG settings to output debug messages. +function dbg(...args) { + if (!runtimeDebug && typeof runtimeDebug != 'undefined') return; + // TODO(sbc): Make this configurable somehow. Its not always convenient for + // logging to show up as warnings. + console.warn(...args); +} + // Endianness check -(function() { +(() => { var h16 = new Int16Array(1); var h8 = new Int8Array(h16.buffer); h16[0] = 0x6373; - if (h8[0] !== 0x73 || h8[1] !== 0x63) throw 'Runtime error: expected the system to be little-endian! (Run with -sSUPPORT_BIG_ENDIAN to bypass)'; + if (h8[0] !== 0x73 || h8[1] !== 0x63) abort('Runtime error: expected the system to be little-endian! (Run with -sSUPPORT_BIG_ENDIAN to bypass)'); })(); -// end include: runtime_assertions.js -var __ATPRERUN__ = []; // functions called before the runtime is initialized -var __ATINIT__ = []; // functions called during startup -var __ATMAIN__ = []; // functions called when main() is to be run -var __ATEXIT__ = []; // functions called during shutdown -var __ATPOSTRUN__ = []; // functions called after the main() is called +function consumedModuleProp(prop) { + if (!Object.getOwnPropertyDescriptor(Module, prop)) { + Object.defineProperty(Module, prop, { + configurable: true, + set() { + abort(`Attempt to set \`Module.${prop}\` after it has already been processed. This can happen, for example, when code is injected via '--post-js' rather than '--pre-js'`); + + } + }); + } +} + +function makeInvalidEarlyAccess(name) { + return () => assert(false, `call to '${name}' via reference taken before Wasm module initialization`); + +} + +function ignoredModuleProp(prop) { + if (Object.getOwnPropertyDescriptor(Module, prop)) { + abort(`\`Module.${prop}\` was supplied but \`${prop}\` not included in INCOMING_MODULE_JS_API`); + } +} + +// forcing the filesystem exports a few things by default +function isExportedByForceFilesystem(name) { + return name === 'FS_createPath' || + name === 'FS_createDataFile' || + name === 'FS_createPreloadedFile' || + name === 'FS_preloadFile' || + name === 'FS_unlink' || + name === 'addRunDependency' || + // The old FS has some functionality that WasmFS lacks. + name === 'FS_createLazyFile' || + name === 'FS_createDevice' || + name === 'removeRunDependency'; +} + +/** + * Intercept access to a symbols in the global symbol. This enables us to give + * informative warnings/errors when folks attempt to use symbols they did not + * include in their build, or no symbols that no longer exist. + * + * We don't define this in MODULARIZE mode since in that mode emscripten symbols + * are never placed in the global scope. + */ +function hookGlobalSymbolAccess(sym, func) { + if (!Object.getOwnPropertyDescriptor(globalThis, sym)) { + Object.defineProperty(globalThis, sym, { + configurable: true, + get() { + func(); + return undefined; + } + }); + } +} + +function missingGlobal(sym, msg) { + hookGlobalSymbolAccess(sym, () => { + warnOnce(`\`${sym}\` is no longer defined by emscripten. ${msg}`); + }); +} + +missingGlobal('buffer', 'Please use HEAP8.buffer or wasmMemory.buffer'); +missingGlobal('asm', 'Please use wasmExports instead'); + +function missingLibrarySymbol(sym) { + hookGlobalSymbolAccess(sym, () => { + // Can't `abort()` here because it would break code that does runtime + // checks. e.g. `if (typeof SDL === 'undefined')`. + var msg = `\`${sym}\` is a library symbol and not included by default; add it to your library.js __deps or to DEFAULT_LIBRARY_FUNCS_TO_INCLUDE on the command line`; + // DEFAULT_LIBRARY_FUNCS_TO_INCLUDE requires the name as it appears in + // library.js, which means $name for a JS name with no prefix, or name + // for a JS name like _name. + var librarySymbol = sym; + if (!librarySymbol.startsWith('_')) { + librarySymbol = '$' + sym; + } + msg += ` (e.g. -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='${librarySymbol}')`; + if (isExportedByForceFilesystem(sym)) { + msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you'; + } + warnOnce(msg); + }); + + // Any symbol that is not included from the JS library is also (by definition) + // not exported on the Module object. + unexportedRuntimeSymbol(sym); +} + +function unexportedRuntimeSymbol(sym) { + if (!Object.getOwnPropertyDescriptor(Module, sym)) { + Object.defineProperty(Module, sym, { + configurable: true, + get() { + var msg = `'${sym}' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the Emscripten FAQ)`; + if (isExportedByForceFilesystem(sym)) { + msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you'; + } + abort(msg); + }, + }); + } +} + +// end include: runtime_debug.js +// Memory management var runtimeInitialized = false; + + +function updateMemoryViews() { + var b = wasmMemory.buffer; + HEAP8 = new Int8Array(b); + HEAP16 = new Int16Array(b); + HEAPU8 = new Uint8Array(b); + HEAPU16 = new Uint16Array(b); + HEAP32 = new Int32Array(b); + HEAPU32 = new Uint32Array(b); + HEAPF32 = new Float32Array(b); + HEAPF64 = new Float64Array(b); + HEAP64 = new BigInt64Array(b); + HEAPU64 = new BigUint64Array(b); +} + +// include: memoryprofiler.js +// end include: memoryprofiler.js +// end include: runtime_common.js +assert(globalThis.Int32Array && globalThis.Float64Array && Int32Array.prototype.subarray && Int32Array.prototype.set, + 'JS engine does not provide full typed array support'); + function preRun() { if (Module['preRun']) { if (typeof Module['preRun'] == 'function') Module['preRun'] = [Module['preRun']]; @@ -503,7 +492,10 @@ function preRun() { addOnPreRun(Module['preRun'].shift()); } } - callRuntimeCallbacks(__ATPRERUN__); + consumedModuleProp('preRun'); + // Begin ATPRERUNS hooks + callRuntimeCallbacks(onPreRuns); + // End ATPRERUNS hooks } function initRuntime() { @@ -512,23 +504,26 @@ function initRuntime() { checkStackCookie(); + // Begin ATINITS hooks + if (!Module['noFSInit'] && !FS.initialized) FS.init(); +TTY.init(); + // End ATINITS hooks -if (!Module["noFSInit"] && !FS.init.initialized) - FS.init(); -FS.ignorePermissions = false; + wasmExports['__wasm_call_ctors'](); -TTY.init(); - callRuntimeCallbacks(__ATINIT__); + // Begin ATPOSTCTORS hooks + FS.ignorePermissions = false; + // End ATPOSTCTORS hooks } function preMain() { checkStackCookie(); - - callRuntimeCallbacks(__ATMAIN__); + // No ATMAINS hooks } function postRun() { checkStackCookie(); + // PThreads reuse the runtime from the main thread. if (Module['postRun']) { if (typeof Module['postRun'] == 'function') Module['postRun'] = [Module['postRun']]; @@ -536,138 +531,25 @@ function postRun() { addOnPostRun(Module['postRun'].shift()); } } + consumedModuleProp('postRun'); - callRuntimeCallbacks(__ATPOSTRUN__); -} - -function addOnPreRun(cb) { - __ATPRERUN__.unshift(cb); -} - -function addOnInit(cb) { - __ATINIT__.unshift(cb); -} - -function addOnPreMain(cb) { - __ATMAIN__.unshift(cb); -} - -function addOnExit(cb) { -} - -function addOnPostRun(cb) { - __ATPOSTRUN__.unshift(cb); -} - -// include: runtime_math.js -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/fround - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32 - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc - -assert(Math.imul, 'This browser does not support Math.imul(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill'); -assert(Math.fround, 'This browser does not support Math.fround(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill'); -assert(Math.clz32, 'This browser does not support Math.clz32(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill'); -assert(Math.trunc, 'This browser does not support Math.trunc(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill'); -// end include: runtime_math.js -// A counter of dependencies for calling run(). If we need to -// do asynchronous work before running, increment this and -// decrement it. Incrementing must happen in a place like -// Module.preRun (used by emcc to add file preloading). -// Note that you can add dependencies in preRun, even though -// it happens right before run - run will be postponed until -// the dependencies are met. -var runDependencies = 0; -var runDependencyWatcher = null; -var dependenciesFulfilled = null; // overridden to take different actions when all run dependencies are fulfilled -var runDependencyTracking = {}; - -function getUniqueRunDependency(id) { - var orig = id; - while (1) { - if (!runDependencyTracking[id]) return id; - id = orig + Math.random(); - } -} - -function addRunDependency(id) { - runDependencies++; - - if (Module['monitorRunDependencies']) { - Module['monitorRunDependencies'](runDependencies); - } - - if (id) { - assert(!runDependencyTracking[id]); - runDependencyTracking[id] = 1; - if (runDependencyWatcher === null && typeof setInterval != 'undefined') { - // Check for missing dependencies every few seconds - runDependencyWatcher = setInterval(() => { - if (ABORT) { - clearInterval(runDependencyWatcher); - runDependencyWatcher = null; - return; - } - var shown = false; - for (var dep in runDependencyTracking) { - if (!shown) { - shown = true; - err('still waiting on run dependencies:'); - } - err(`dependency: ${dep}`); - } - if (shown) { - err('(end of list)'); - } - }, 10000); - } - } else { - err('warning: run dependency added without ID'); - } -} - -function removeRunDependency(id) { - runDependencies--; - - if (Module['monitorRunDependencies']) { - Module['monitorRunDependencies'](runDependencies); - } - - if (id) { - assert(runDependencyTracking[id]); - delete runDependencyTracking[id]; - } else { - err('warning: run dependency removed without ID'); - } - if (runDependencies == 0) { - if (runDependencyWatcher !== null) { - clearInterval(runDependencyWatcher); - runDependencyWatcher = null; - } - if (dependenciesFulfilled) { - var callback = dependenciesFulfilled; - dependenciesFulfilled = null; - callback(); // can add another dependenciesFulfilled - } - } + // Begin ATPOSTRUNS hooks + callRuntimeCallbacks(onPostRuns); + // End ATPOSTRUNS hooks } -/** @param {string|number=} what */ +/** + * @param {string|number=} what + */ function abort(what) { - if (Module['onAbort']) { - Module['onAbort'](what); - } + Module['onAbort']?.(what); - what = 'Aborted(' + what + ')'; + what = `Aborted(${what})`; // TODO(sbc): Should we remove printing and leave it up to whoever // catches the exception? err(what); ABORT = true; - EXITSTATUS = 1; // Use a wasm runtime error, because a JS error might be seen as a foreign // exception, which means we'd run destructors on it. We need the error to @@ -679,7 +561,7 @@ function abort(what) { // allows this in the wasm spec. // Suppress closure compiler warning here. Closure compiler's builtin extern - // defintion for WebAssembly.RuntimeError claims it takes no arguments even + // definition for WebAssembly.RuntimeError claims it takes no arguments even // though it can. // TODO(https://github.com/google/closure-compiler/pull/3913): Remove if/when upstream closure gets fixed. /** @suppress {checkTypes} */ @@ -691,41 +573,23 @@ function abort(what) { throw e; } -// include: memoryprofiler.js -// end include: memoryprofiler.js -// include: URIUtils.js -// Prefix of data URIs emitted by SINGLE_FILE and related options. -var dataURIPrefix = 'data:application/octet-stream;base64,'; +function createExportWrapper(name, nargs) { + return (...args) => { + assert(runtimeInitialized, `native function \`${name}\` called before runtime initialization`); + var f = wasmExports[name]; + assert(f, `exported native function \`${name}\` not found`); + // Only assert for too many arguments. Too few can be valid since the missing arguments will be zero filled. + assert(args.length <= nargs, `native function \`${name}\` called with ${args.length} args but expects ${nargs}`); + return f(...args); + }; +} -/** - * Indicates whether filename is a base64 data URI. - * @noinline - */ -var isDataURI = (filename) => filename.startsWith(dataURIPrefix); +var wasmBinaryFile; -/** - * Indicates whether filename is delivered via file protocol (as opposed to http/https) - * @noinline - */ -var isFileURI = (filename) => filename.startsWith('file://'); -// end include: URIUtils.js -function createExportWrapper(name) { - return function() { - assert(runtimeInitialized, `native function \`${name}\` called before runtime initialization`); - var f = wasmExports[name]; - assert(f, `exported native function \`${name}\` not found`); - return f.apply(null, arguments); - }; +function findWasmBinary() { + return locateFile('ca65.wasm'); } -// include: runtime_exceptions.js -// end include: runtime_exceptions.js -var wasmBinaryFile; - wasmBinaryFile = 'ca65.wasm'; - if (!isDataURI(wasmBinaryFile)) { - wasmBinaryFile = locateFile(wasmBinaryFile); - } - function getBinarySync(file) { if (file == wasmBinaryFile && wasmBinary) { return new Uint8Array(wasmBinary); @@ -733,99 +597,82 @@ function getBinarySync(file) { if (readBinary) { return readBinary(file); } - throw "both async and sync fetching of the wasm failed"; + // Throwing a plain string here, even though it not normally advisable since + // this gets turning into an `abort` in instantiateArrayBuffer. + throw 'both async and sync fetching of the wasm failed'; } -function getBinaryPromise(binaryFile) { - // If we don't have the binary yet, try to load it asynchronously. - // Fetch has some additional restrictions over XHR, like it can't be used on a file:// url. - // See https://github.com/github/fetch/pull/92#issuecomment-140665932 - // Cordova or Electron apps are typically loaded from a file:// url. - // So use fetch if it is available and the url is not a file, otherwise fall back to XHR. - if (!wasmBinary - && (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER)) { - if (typeof fetch == 'function' - && !isFileURI(binaryFile) - ) { - return fetch(binaryFile, { credentials: 'same-origin' }).then((response) => { - if (!response['ok']) { - throw "failed to load wasm binary file at '" + binaryFile + "'"; - } - return response['arrayBuffer'](); - }).catch(() => getBinarySync(binaryFile)); - } - else if (readAsync) { - // fetch is not available or url is file => try XHR (readAsync uses XHR internally) - return new Promise((resolve, reject) => { - readAsync(binaryFile, (response) => resolve(new Uint8Array(/** @type{!ArrayBuffer} */(response))), reject) - }); +async function getWasmBinary(binaryFile) { + // If we don't have the binary yet, load it asynchronously using readAsync. + if (!wasmBinary) { + // Fetch the binary using readAsync + try { + var response = await readAsync(binaryFile); + return new Uint8Array(response); + } catch { + // Fall back to getBinarySync below; } } // Otherwise, getBinarySync should be able to get it synchronously - return Promise.resolve().then(() => getBinarySync(binaryFile)); + return getBinarySync(binaryFile); } -function instantiateArrayBuffer(binaryFile, imports, receiver) { - return getBinaryPromise(binaryFile).then((binary) => { - return WebAssembly.instantiate(binary, imports); - }).then((instance) => { +async function instantiateArrayBuffer(binaryFile, imports) { + try { + var binary = await getWasmBinary(binaryFile); + var instance = await WebAssembly.instantiate(binary, imports); return instance; - }).then(receiver, (reason) => { + } catch (reason) { err(`failed to asynchronously prepare wasm: ${reason}`); // Warn on some common problems. - if (isFileURI(wasmBinaryFile)) { - err(`warning: Loading from a file URI (${wasmBinaryFile}) is not supported in most browsers. See https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-run-a-local-webserver-for-testing-why-does-my-program-stall-in-downloading-or-preparing`); + if (isFileURI(binaryFile)) { + err(`warning: Loading from a file URI (${binaryFile}) is not supported in most browsers. See https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-run-a-local-webserver-for-testing-why-does-my-program-stall-in-downloading-or-preparing`); } abort(reason); - }); + } } -function instantiateAsync(binary, binaryFile, imports, callback) { - if (!binary && - typeof WebAssembly.instantiateStreaming == 'function' && - !isDataURI(binaryFile) && +async function instantiateAsync(binary, binaryFile, imports) { + if (!binary // Don't use streaming for file:// delivered objects in a webview, fetch them synchronously. - !isFileURI(binaryFile) && + && !isFileURI(binaryFile) // Avoid instantiateStreaming() on Node.js environment for now, as while // Node.js v18.1.0 implements it, it does not have a full fetch() // implementation yet. // // Reference: // https://github.com/emscripten-core/emscripten/pull/16917 - !ENVIRONMENT_IS_NODE && - typeof fetch == 'function') { - return fetch(binaryFile, { credentials: 'same-origin' }).then((response) => { - // Suppress closure warning here since the upstream definition for - // instantiateStreaming only allows Promise rather than - // an actual Response. - // TODO(https://github.com/google/closure-compiler/pull/3913): Remove if/when upstream closure is fixed. - /** @suppress {checkTypes} */ - var result = WebAssembly.instantiateStreaming(response, imports); - - return result.then( - callback, - function(reason) { - // We expect the most common failure cause to be a bad MIME type for the binary, - // in which case falling back to ArrayBuffer instantiation should work. - err(`wasm streaming compile failed: ${reason}`); - err('falling back to ArrayBuffer instantiation'); - return instantiateArrayBuffer(binaryFile, imports, callback); - }); - }); + && !ENVIRONMENT_IS_NODE + ) { + try { + var response = fetch(binaryFile, { credentials: 'same-origin' }); + var instantiationResult = await WebAssembly.instantiateStreaming(response, imports); + return instantiationResult; + } catch (reason) { + // We expect the most common failure cause to be a bad MIME type for the binary, + // in which case falling back to ArrayBuffer instantiation should work. + err(`wasm streaming compile failed: ${reason}`); + err('falling back to ArrayBuffer instantiation'); + // fall back of instantiateArrayBuffer below + }; } - return instantiateArrayBuffer(binaryFile, imports, callback); + return instantiateArrayBuffer(binaryFile, imports); } -// Create the wasm instance. -// Receives the wasm imports, returns the exports. -function createWasm() { +function getWasmImports() { // prepare imports - var info = { + var imports = { 'env': wasmImports, 'wasi_snapshot_preview1': wasmImports, }; + return imports; +} + +// Create the wasm instance. +// Receives the wasm imports, returns the exports. +async function createWasm() { // Load the wasm module and create an instance of using native support in the JS engine. // handle a generated wasm instance, receiving its exports and // performing other necessary setup @@ -833,23 +680,13 @@ function createWasm() { function receiveInstance(instance, module) { wasmExports = instance.exports; + assignWasmExports(wasmExports); - - wasmMemory = wasmExports['memory']; - - assert(wasmMemory, "memory not found in wasm exports"); - // This assertion doesn't hold when emscripten is run in --post-link - // mode. - // TODO(sbc): Read INITIAL_MEMORY out of the wasm file in post-link mode. - //assert(wasmMemory.buffer.byteLength === 16777216); updateMemoryViews(); - addOnInit(wasmExports['__wasm_call_ctors']); - removeRunDependency('wasm-instantiate'); return wasmExports; } - // wait for the pthread pool (if any) addRunDependency('wasm-instantiate'); // Prefer streaming instantiation if available. @@ -864,9 +701,11 @@ function createWasm() { trueModule = null; // TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193, the above line no longer optimizes out down to the following line. // When the regression is fixed, can restore the above PTHREADS-enabled path. - receiveInstance(result['instance']); + return receiveInstance(result['instance']); } + var info = getWasmImports(); + // User shell pages can write their own Module.instantiateWasm = function(imports, successCallback) callback // to manually instantiate the Wasm module themselves. This allows pages to // run the instantiation parallel to any other async startup actions they are @@ -874,132 +713,66 @@ function createWasm() { // Also pthreads and wasm workers initialize the wasm instance through this // path. if (Module['instantiateWasm']) { - - try { - return Module['instantiateWasm'](info, receiveInstance); - } catch(e) { - err(`Module.instantiateWasm callback failed with error: ${e}`); - return false; - } + return new Promise((resolve, reject) => { + try { + Module['instantiateWasm'](info, (inst, mod) => { + resolve(receiveInstance(inst, mod)); + }); + } catch(e) { + err(`Module.instantiateWasm callback failed with error: ${e}`); + reject(e); + } + }); } - instantiateAsync(wasmBinary, wasmBinaryFile, info, receiveInstantiationResult); - return {}; // no exports yet; we'll fill them in later + wasmBinaryFile ??= findWasmBinary(); + var result = await instantiateAsync(wasmBinary, wasmBinaryFile, info); + var exports = receiveInstantiationResult(result); + return exports; } -// Globals used by JS i64 conversions (see makeSetValue) -var tempDouble; -var tempI64; +// end include: preamble.js -// include: runtime_debug.js -function legacyModuleProp(prop, newName, incomming=true) { - if (!Object.getOwnPropertyDescriptor(Module, prop)) { - Object.defineProperty(Module, prop, { - configurable: true, - get() { - let extra = incomming ? ' (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)' : ''; - abort(`\`Module.${prop}\` has been replaced by \`${newName}\`` + extra); +// Begin JS library code + + class ExitStatus { + name = 'ExitStatus'; + constructor(status) { + this.message = `Program terminated with exit(${status})`; + this.status = status; } - }); - } -} + } -function ignoredModuleProp(prop) { - if (Object.getOwnPropertyDescriptor(Module, prop)) { - abort(`\`Module.${prop}\` was supplied but \`${prop}\` not included in INCOMING_MODULE_JS_API`); - } -} + /** @type {!Int16Array} */ + var HEAP16; -// forcing the filesystem exports a few things by default -function isExportedByForceFilesystem(name) { - return name === 'FS_createPath' || - name === 'FS_createDataFile' || - name === 'FS_createPreloadedFile' || - name === 'FS_unlink' || - name === 'addRunDependency' || - // The old FS has some functionality that WasmFS lacks. - name === 'FS_createLazyFile' || - name === 'FS_createDevice' || - name === 'removeRunDependency'; -} + /** @type {!Int32Array} */ + var HEAP32; -function missingGlobal(sym, msg) { - if (typeof globalThis !== 'undefined') { - Object.defineProperty(globalThis, sym, { - configurable: true, - get() { - warnOnce('`' + sym + '` is not longer defined by emscripten. ' + msg); - return undefined; - } - }); - } -} + /** not-@type {!BigInt64Array} */ + var HEAP64; -missingGlobal('buffer', 'Please use HEAP8.buffer or wasmMemory.buffer'); -missingGlobal('asm', 'Please use wasmExports instead'); + /** @type {!Int8Array} */ + var HEAP8; -function missingLibrarySymbol(sym) { - if (typeof globalThis !== 'undefined' && !Object.getOwnPropertyDescriptor(globalThis, sym)) { - Object.defineProperty(globalThis, sym, { - configurable: true, - get() { - // Can't `abort()` here because it would break code that does runtime - // checks. e.g. `if (typeof SDL === 'undefined')`. - var msg = '`' + sym + '` is a library symbol and not included by default; add it to your library.js __deps or to DEFAULT_LIBRARY_FUNCS_TO_INCLUDE on the command line'; - // DEFAULT_LIBRARY_FUNCS_TO_INCLUDE requires the name as it appears in - // library.js, which means $name for a JS name with no prefix, or name - // for a JS name like _name. - var librarySymbol = sym; - if (!librarySymbol.startsWith('_')) { - librarySymbol = '$' + sym; - } - msg += " (e.g. -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='" + librarySymbol + "')"; - if (isExportedByForceFilesystem(sym)) { - msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you'; - } - warnOnce(msg); - return undefined; - } - }); - } - // Any symbol that is not included from the JS libary is also (by definition) - // not exported on the Module object. - unexportedRuntimeSymbol(sym); -} + /** @type {!Float32Array} */ + var HEAPF32; -function unexportedRuntimeSymbol(sym) { - if (!Object.getOwnPropertyDescriptor(Module, sym)) { - Object.defineProperty(Module, sym, { - configurable: true, - get() { - var msg = "'" + sym + "' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the Emscripten FAQ)"; - if (isExportedByForceFilesystem(sym)) { - msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you'; - } - abort(msg); - } - }); - } -} + /** @type {!Float64Array} */ + var HEAPF64; -// Used by XXXXX_DEBUG settings to output debug messages. -function dbg(text) { - // TODO(sbc): Make this configurable somehow. Its not always convenient for - // logging to show up as warnings. - console.warn.apply(console, arguments); -} -// end include: runtime_debug.js -// === Body === + /** @type {!Uint16Array} */ + var HEAPU16; -// end include: preamble.js + /** @type {!Uint32Array} */ + var HEAPU32; - /** @constructor */ - function ExitStatus(status) { - this.name = 'ExitStatus'; - this.message = `Program terminated with exit(${status})`; - this.status = status; - } + /** not-@type {!BigUint64Array} */ + var HEAPU64; + + /** @type {!Uint8Array} */ + var HEAPU8; var callRuntimeCallbacks = (callbacks) => { while (callbacks.length > 0) { @@ -1007,20 +780,91 @@ function dbg(text) { callbacks.shift()(Module); } }; + var onPostRuns = []; + var addOnPostRun = (cb) => onPostRuns.push(cb); + + var onPreRuns = []; + var addOnPreRun = (cb) => onPreRuns.push(cb); + + var runDependencies = 0; + + + var dependenciesFulfilled = null; + + var runDependencyTracking = { + }; + + var runDependencyWatcher = null; + var removeRunDependency = (id) => { + runDependencies--; + + Module['monitorRunDependencies']?.(runDependencies); + + assert(id, 'removeRunDependency requires an ID'); + assert(runDependencyTracking[id]); + delete runDependencyTracking[id]; + if (runDependencies == 0) { + if (runDependencyWatcher !== null) { + clearInterval(runDependencyWatcher); + runDependencyWatcher = null; + } + if (dependenciesFulfilled) { + var callback = dependenciesFulfilled; + dependenciesFulfilled = null; + callback(); // can add another dependenciesFulfilled + } + } + }; + + + var addRunDependency = (id) => { + runDependencies++; + + Module['monitorRunDependencies']?.(runDependencies); + + assert(id, 'addRunDependency requires an ID') + assert(!runDependencyTracking[id]); + runDependencyTracking[id] = 1; + if (runDependencyWatcher === null && globalThis.setInterval) { + // Check for missing dependencies every few seconds + runDependencyWatcher = setInterval(() => { + if (ABORT) { + clearInterval(runDependencyWatcher); + runDependencyWatcher = null; + return; + } + var shown = false; + for (var dep in runDependencyTracking) { + if (!shown) { + shown = true; + err('still waiting on run dependencies:'); + } + err(`dependency: ${dep}`); + } + if (shown) { + err('(end of list)'); + } + }, 10000); + // Prevent this timer from keeping the runtime alive if nothing + // else is. + runDependencyWatcher.unref?.() + } + }; + /** - * @param {number} ptr - * @param {string} type - */ + * @param {number} ptr + * @param {string} type + */ function getValue(ptr, type = 'i8') { if (type.endsWith('*')) type = '*'; switch (type) { - case 'i1': return HEAP8[((ptr)>>0)]; - case 'i8': return HEAP8[((ptr)>>0)]; + case 'i1': return HEAP8[ptr]; + case 'i8': return HEAP8[ptr]; case 'i16': return HEAP16[((ptr)>>1)]; case 'i32': return HEAP32[((ptr)>>2)]; - case 'i64': abort('to do getValue(i64) use WASM_BIGINT'); + case 'i64': return HEAP64[((ptr)>>3)]; case 'float': return HEAPF32[((ptr)>>2)]; case 'double': return HEAPF64[((ptr)>>3)]; case '*': return HEAPU32[((ptr)>>2)]; @@ -1028,29 +872,30 @@ function dbg(text) { } } - var noExitRuntime = Module['noExitRuntime'] || true; + var noExitRuntime = true; - var ptrToString = (ptr) => { - assert(typeof ptr === 'number'); - // With CAN_ADDRESS_2GB or MEMORY64, pointers are already unsigned. + function ptrToString(ptr) { + assert(typeof ptr === 'number', `ptrToString expects a number, got ${typeof ptr}`); + // Convert to 32-bit unsigned value ptr >>>= 0; return '0x' + ptr.toString(16).padStart(8, '0'); - }; + } + /** - * @param {number} ptr - * @param {number} value - * @param {string} type - */ + * @param {number} ptr + * @param {number} value + * @param {string} type + */ function setValue(ptr, value, type = 'i8') { if (type.endsWith('*')) type = '*'; switch (type) { - case 'i1': HEAP8[((ptr)>>0)] = value; break; - case 'i8': HEAP8[((ptr)>>0)] = value; break; + case 'i1': HEAP8[ptr] = value; break; + case 'i8': HEAP8[ptr] = value; break; case 'i16': HEAP16[((ptr)>>1)] = value; break; case 'i32': HEAP32[((ptr)>>2)] = value; break; - case 'i64': abort('to do setValue(i64) use WASM_BIGINT'); + case 'i64': HEAP64[((ptr)>>3)] = BigInt(value); break; case 'float': HEAPF32[((ptr)>>2)] = value; break; case 'double': HEAPF64[((ptr)>>3)] = value; break; case '*': HEAPU32[((ptr)>>2)] = value; break; @@ -1058,8 +903,12 @@ function dbg(text) { } } + var stackRestore = (val) => __emscripten_stack_restore(val); + + var stackSave = () => _emscripten_stack_get_current(); + var warnOnce = (text) => { - if (!warnOnce.shown) warnOnce.shown = {}; + warnOnce.shown ||= {}; if (!warnOnce.shown[text]) { warnOnce.shown[text] = 1; if (ENVIRONMENT_IS_NODE) text = 'warning: ' + text; @@ -1067,6 +916,8 @@ function dbg(text) { } }; + + var PATH = { isAbs:(path) => path.charAt(0) === '/', splitPath:(filename) => { @@ -1098,7 +949,7 @@ function dbg(text) { }, normalize:(path) => { var isAbsolute = PATH.isAbs(path), - trailingSlash = path.substr(-1) === '/'; + trailingSlash = path.slice(-1) === '/'; // Normalize the path path = PATH.normalizeArray(path.split('/').filter((p) => !!p), !isAbsolute).join('/'); if (!path && !isAbsolute) { @@ -1119,143 +970,116 @@ function dbg(text) { } if (dir) { // It has a dirname, strip trailing slash - dir = dir.substr(0, dir.length - 1); + dir = dir.slice(0, -1); } return root + dir; }, - basename:(path) => { - // EMSCRIPTEN return '/'' for '/', not an empty string - if (path === '/') return '/'; - path = PATH.normalize(path); - path = path.replace(/\/$/, ""); - var lastSlash = path.lastIndexOf('/'); - if (lastSlash === -1) return path; - return path.substr(lastSlash+1); - }, - join:function() { - var paths = Array.prototype.slice.call(arguments); - return PATH.normalize(paths.join('/')); - }, - join2:(l, r) => PATH.normalize(l + '/' + r), - }; + basename:(path) => path && path.match(/([^\/]+|\/)\/*$/)[1], +join:(...paths) => PATH.normalize(paths.join('/')), +join2:(l, r) => PATH.normalize(l + '/' + r), +}; - var initRandomFill = () => { - if (typeof crypto == 'object' && typeof crypto['getRandomValues'] == 'function') { - // for modern web browsers - return (view) => crypto.getRandomValues(view); - } else - if (ENVIRONMENT_IS_NODE) { - // for nodejs with or without crypto support included - try { - var crypto_module = require('crypto'); - var randomFillSync = crypto_module['randomFillSync']; - if (randomFillSync) { - // nodejs with LTS crypto support - return (view) => crypto_module['randomFillSync'](view); - } - // very old nodejs with the original crypto API - var randomBytes = crypto_module['randomBytes']; - return (view) => ( - view.set(randomBytes(view.byteLength)), - // Return the original view to match modern native implementations. - view - ); - } catch (e) { - // nodejs doesn't have crypto support - } - } - // we couldn't find a proper implementation, as Math.random() is not suitable for /dev/random, see emscripten-core/emscripten/pull/7096 - abort("no cryptographic support found for randomDevice. consider polyfilling it if you want to use something insecure like Math.random(), e.g. put this in a --pre-js: var crypto = { getRandomValues: (array) => { for (var i = 0; i < array.length; i++) array[i] = (Math.random()*256)|0 } };"); - }; - var randomFill = (view) => { - // Lazily init on the first invocation. - return (randomFill = initRandomFill())(view); - }; +var initRandomFill = () => { + // This block is not needed on v19+ since crypto.getRandomValues is builtin + if (ENVIRONMENT_IS_NODE) { + var nodeCrypto = require('node:crypto'); + return (view) => nodeCrypto.randomFillSync(view); + } + return (view) => (crypto.getRandomValues(view), 0); + }; +var randomFill = (view) => (randomFill = initRandomFill())(view); - var PATH_FS = { - resolve:function() { - var resolvedPath = '', - resolvedAbsolute = false; - for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { - var path = (i >= 0) ? arguments[i] : FS.cwd(); - // Skip empty and invalid entries - if (typeof path != 'string') { - throw new TypeError('Arguments to path.resolve must be strings'); - } else if (!path) { - return ''; // an invalid portion invalidates the whole thing - } - resolvedPath = path + '/' + resolvedPath; - resolvedAbsolute = PATH.isAbs(path); - } - // At this point the path should be resolved to a full absolute path, but - // handle relative paths to be safe (might happen when process.cwd() fails) - resolvedPath = PATH.normalizeArray(resolvedPath.split('/').filter((p) => !!p), !resolvedAbsolute).join('/'); - return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; - }, - relative:(from, to) => { - from = PATH_FS.resolve(from).substr(1); - to = PATH_FS.resolve(to).substr(1); - function trim(arr) { - var start = 0; - for (; start < arr.length; start++) { - if (arr[start] !== '') break; - } - var end = arr.length - 1; - for (; end >= 0; end--) { - if (arr[end] !== '') break; - } - if (start > end) return []; - return arr.slice(start, end - start + 1); - } - var fromParts = trim(from.split('/')); - var toParts = trim(to.split('/')); - var length = Math.min(fromParts.length, toParts.length); - var samePartsLength = length; - for (var i = 0; i < length; i++) { - if (fromParts[i] !== toParts[i]) { - samePartsLength = i; - break; - } + +var PATH_FS = { +resolve:(...args) => { + var resolvedPath = '', + resolvedAbsolute = false; + for (var i = args.length - 1; i >= -1 && !resolvedAbsolute; i--) { + var path = (i >= 0) ? args[i] : FS.cwd(); + // Skip empty and invalid entries + if (typeof path != 'string') { + throw new TypeError('Arguments to path.resolve must be strings'); + } else if (!path) { + return ''; // an invalid portion invalidates the whole thing } - var outputParts = []; - for (var i = samePartsLength; i < fromParts.length; i++) { - outputParts.push('..'); + resolvedPath = path + '/' + resolvedPath; + resolvedAbsolute = PATH.isAbs(path); + } + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + resolvedPath = PATH.normalizeArray(resolvedPath.split('/').filter((p) => !!p), !resolvedAbsolute).join('/'); + return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; + }, +relative:(from, to) => { + from = PATH_FS.resolve(from).slice(1); + to = PATH_FS.resolve(to).slice(1); + function trim(arr) { + var start = 0; + for (; start < arr.length; start++) { + if (arr[start] !== '') break; + } + var end = arr.length - 1; + for (; end >= 0; end--) { + if (arr[end] !== '') break; + } + if (start > end) return []; + return arr.slice(start, end - start + 1); + } + var fromParts = trim(from.split('/')); + var toParts = trim(to.split('/')); + var length = Math.min(fromParts.length, toParts.length); + var samePartsLength = length; + for (var i = 0; i < length; i++) { + if (fromParts[i] !== toParts[i]) { + samePartsLength = i; + break; } - outputParts = outputParts.concat(toParts.slice(samePartsLength)); - return outputParts.join('/'); - }, - }; + } + var outputParts = []; + for (var i = samePartsLength; i < fromParts.length; i++) { + outputParts.push('..'); + } + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + return outputParts.join('/'); + }, +}; - var UTF8Decoder = typeof TextDecoder != 'undefined' ? new TextDecoder('utf8') : undefined; +var UTF8Decoder = globalThis.TextDecoder && new TextDecoder(); - /** - * Given a pointer 'idx' to a null-terminated UTF8-encoded string in the given - * array that contains uint8 values, returns a copy of that string as a - * Javascript String object. - * heapOrArray is either a regular array, or a JavaScript typed array view. - * @param {number} idx - * @param {number=} maxBytesToRead - * @return {string} - */ - var UTF8ArrayToString = (heapOrArray, idx, maxBytesToRead) => { - var endIdx = idx + maxBytesToRead; - var endPtr = idx; - // TextDecoder needs to know the byte length in advance, it doesn't stop on - // null terminator by itself. Also, use the length info to avoid running tiny - // strings through TextDecoder, since .subarray() allocates garbage. - // (As a tiny code save trick, compare endPtr against endIdx using a negation, - // so that undefined means Infinity) - while (heapOrArray[endPtr] && !(endPtr >= endIdx)) ++endPtr; +var findStringEnd = (heapOrArray, idx, maxBytesToRead, ignoreNul) => { + var maxIdx = idx + maxBytesToRead; + if (ignoreNul) return maxIdx; + // TextDecoder needs to know the byte length in advance, it doesn't stop on + // null terminator by itself. + // As a tiny code save trick, compare idx against maxIdx using a negation, + // so that maxBytesToRead=undefined/NaN means Infinity. + while (heapOrArray[idx] && !(idx >= maxIdx)) ++idx; + return idx; + }; + + /** + * Given a pointer 'idx' to a null-terminated UTF8-encoded string in the given + * array that contains uint8 values, returns a copy of that string as a + * Javascript String object. + * heapOrArray is either a regular array, or a JavaScript typed array view. + * @param {number=} idx + * @param {number=} maxBytesToRead + * @param {boolean=} ignoreNul - If true, the function will not stop on a NUL character. + * @return {string} + */ + var UTF8ArrayToString = (heapOrArray, idx = 0, maxBytesToRead, ignoreNul) => { + + var endPtr = findStringEnd(heapOrArray, idx, maxBytesToRead, ignoreNul); + + // When using conditional TextDecoder, skip it for short strings as the overhead of the native call is not worth it. if (endPtr - idx > 16 && heapOrArray.buffer && UTF8Decoder) { return UTF8Decoder.decode(heapOrArray.subarray(idx, endPtr)); } var str = ''; - // If building with TextDecoder, we have already computed the string length - // above, so test loop end condition against that while (idx < endPtr) { // For UTF8 byte structure, see: // http://en.wikipedia.org/wiki/UTF-8#Description @@ -1269,10 +1093,10 @@ function dbg(text) { if ((u0 & 0xF0) == 0xE0) { u0 = ((u0 & 15) << 12) | (u1 << 6) | u2; } else { - if ((u0 & 0xF8) != 0xF0) warnOnce('Invalid UTF-8 leading byte ' + ptrToString(u0) + ' encountered when deserializing a UTF-8 string in wasm memory to a JS string!'); + if ((u0 & 0xF8) != 0xF0) warnOnce(`Invalid UTF-8 leading byte ${ptrToString(u0)} encountered when deserializing a UTF-8 string in wasm memory to a JS string!`); u0 = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | (heapOrArray[idx++] & 63); } - + if (u0 < 0x10000) { str += String.fromCharCode(u0); } else { @@ -1282,9 +1106,9 @@ function dbg(text) { } return str; }; - + var FS_stdin_getChar_buffer = []; - + var lengthBytesUTF8 = (str) => { var len = 0; for (var i = 0; i < str.length; ++i) { @@ -1305,29 +1129,21 @@ function dbg(text) { } return len; }; - + var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { assert(typeof str === 'string', `stringToUTF8Array expects a string (got ${typeof str})`); // Parameter maxBytesToWrite is not optional. Negative values, 0, null, // undefined and false each don't write out any bytes. if (!(maxBytesToWrite > 0)) return 0; - + var startIdx = outIdx; var endIdx = outIdx + maxBytesToWrite - 1; // -1 for string null terminator. for (var i = 0; i < str.length; ++i) { - // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code - // unit, not a Unicode code point of the character! So decode - // UTF16->UTF32->UTF8. - // See http://unicode.org/faq/utf_bom.html#utf16-3 // For UTF8 byte structure, see http://en.wikipedia.org/wiki/UTF-8#Description // and https://www.ietf.org/rfc/rfc2279.txt // and https://tools.ietf.org/html/rfc3629 - var u = str.charCodeAt(i); // possibly a lead surrogate - if (u >= 0xD800 && u <= 0xDFFF) { - var u1 = str.charCodeAt(++i); - u = 0x10000 + ((u & 0x3FF) << 10) | (u1 & 0x3FF); - } + var u = str.codePointAt(i); if (u <= 0x7F) { if (outIdx >= endIdx) break; heap[outIdx++] = u; @@ -1342,11 +1158,14 @@ function dbg(text) { heap[outIdx++] = 0x80 | (u & 63); } else { if (outIdx + 3 >= endIdx) break; - if (u > 0x10FFFF) warnOnce('Invalid Unicode code point ' + ptrToString(u) + ' encountered when serializing a JS string to a UTF-8 string in wasm memory! (Valid unicode code points should be in range 0-0x10FFFF).'); + if (u > 0x10FFFF) warnOnce(`Invalid Unicode code point ${ptrToString(u)} encountered when serializing a JS string to a UTF-8 string in wasm memory! (Valid unicode code points should be in range 0-0x10FFFF).`); heap[outIdx++] = 0xF0 | (u >> 18); heap[outIdx++] = 0x80 | ((u >> 12) & 63); heap[outIdx++] = 0x80 | ((u >> 6) & 63); heap[outIdx++] = 0x80 | (u & 63); + // Gotcha: if codePoint is over 0xFFFF, it is represented as a surrogate pair in UTF-16. + // We need to manually skip over the second code unit for correct iteration. + i++; } } // Null-terminate the pointer to the buffer. @@ -1354,13 +1173,13 @@ function dbg(text) { return outIdx - startIdx; }; /** @type {function(string, boolean=, number=)} */ - function intArrayFromString(stringy, dontAddNull, length) { - var len = length > 0 ? length : lengthBytesUTF8(stringy)+1; - var u8array = new Array(len); - var numBytesWritten = stringToUTF8Array(stringy, u8array, 0, u8array.length); - if (dontAddNull) u8array.length = numBytesWritten; - return u8array; - } + var intArrayFromString = (stringy, dontAddNull, length) => { + var len = length > 0 ? length : lengthBytesUTF8(stringy)+1; + var u8array = new Array(len); + var numBytesWritten = stringToUTF8Array(stringy, u8array, 0, u8array.length); + if (dontAddNull) u8array.length = numBytesWritten; + return u8array; + }; var FS_stdin_getChar = () => { if (!FS_stdin_getChar_buffer.length) { var result = null; @@ -1369,7 +1188,7 @@ function dbg(text) { var BUFSIZE = 256; var buf = Buffer.alloc(BUFSIZE); var bytesRead = 0; - + // For some reason we must suppress a closure warning here, even though // fd definitely exists on process.stdin, and is even the proper way to // get the fd of stdin, @@ -1378,36 +1197,29 @@ function dbg(text) { // so it is related to the surrounding code in some unclear manner. /** @suppress {missingProperties} */ var fd = process.stdin.fd; - + try { - bytesRead = fs.readSync(fd, buf); + bytesRead = fs.readSync(fd, buf, 0, BUFSIZE); } catch(e) { - // Cross-platform differences: on Windows, reading EOF throws an exception, but on other OSes, - // reading EOF returns 0. Uniformize behavior by treating the EOF exception to return 0. + // Cross-platform differences: on Windows, reading EOF throws an + // exception, but on other OSes, reading EOF returns 0. Uniformize + // behavior by treating the EOF exception to return 0. if (e.toString().includes('EOF')) bytesRead = 0; else throw e; } - + if (bytesRead > 0) { result = buf.slice(0, bytesRead).toString('utf-8'); - } else { - result = null; } } else - if (typeof window != 'undefined' && - typeof window.prompt == 'function') { + if (globalThis.window?.prompt) { // Browser. result = window.prompt('Input: '); // returns null on cancel if (result !== null) { result += '\n'; } - } else if (typeof readline == 'function') { - // Command line. - result = readline(); - if (result !== null) { - result += '\n'; - } - } + } else + {} if (!result) { return null; } @@ -1478,7 +1290,7 @@ function dbg(text) { buffer[offset+i] = result; } if (bytesRead) { - stream.node.timestamp = Date.now(); + stream.node.atime = Date.now(); } return bytesRead; }, @@ -1494,7 +1306,7 @@ function dbg(text) { throw new FS.ErrnoError(29); } if (length) { - stream.node.timestamp = Date.now(); + stream.node.mtime = stream.node.ctime = Date.now(); } return i; }, @@ -1505,15 +1317,15 @@ function dbg(text) { }, put_char(tty, val) { if (val === null || val === 10) { - out(UTF8ArrayToString(tty.output, 0)); + out(UTF8ArrayToString(tty.output)); tty.output = []; } else { if (val != 0) tty.output.push(val); // val == 0 would cut text output off in the middle. } }, fsync(tty) { - if (tty.output && tty.output.length > 0) { - out(UTF8ArrayToString(tty.output, 0)); + if (tty.output?.length > 0) { + out(UTF8ArrayToString(tty.output)); tty.output = []; } }, @@ -1542,93 +1354,81 @@ function dbg(text) { default_tty1_ops:{ put_char(tty, val) { if (val === null || val === 10) { - err(UTF8ArrayToString(tty.output, 0)); + err(UTF8ArrayToString(tty.output)); tty.output = []; } else { if (val != 0) tty.output.push(val); } }, fsync(tty) { - if (tty.output && tty.output.length > 0) { - err(UTF8ArrayToString(tty.output, 0)); + if (tty.output?.length > 0) { + err(UTF8ArrayToString(tty.output)); tty.output = []; } }, }, }; - - - var zeroMemory = (address, size) => { - HEAPU8.fill(0, address, address + size); - return address; - }; - - var alignMemory = (size, alignment) => { - assert(alignment, "alignment argument is required"); - return Math.ceil(size / alignment) * alignment; - }; + + var mmapAlloc = (size) => { abort('internal error: mmapAlloc called but `emscripten_builtin_memalign` native symbol not exported'); }; var MEMFS = { ops_table:null, mount(mount) { - return MEMFS.createNode(null, '/', 16384 | 511 /* 0777 */, 0); + return MEMFS.createNode(null, '/', 16895, 0); }, createNode(parent, name, mode, dev) { if (FS.isBlkdev(mode) || FS.isFIFO(mode)) { - // no supported + // not supported throw new FS.ErrnoError(63); } - if (!MEMFS.ops_table) { - MEMFS.ops_table = { - dir: { - node: { - getattr: MEMFS.node_ops.getattr, - setattr: MEMFS.node_ops.setattr, - lookup: MEMFS.node_ops.lookup, - mknod: MEMFS.node_ops.mknod, - rename: MEMFS.node_ops.rename, - unlink: MEMFS.node_ops.unlink, - rmdir: MEMFS.node_ops.rmdir, - readdir: MEMFS.node_ops.readdir, - symlink: MEMFS.node_ops.symlink - }, - stream: { - llseek: MEMFS.stream_ops.llseek - } - }, - file: { - node: { - getattr: MEMFS.node_ops.getattr, - setattr: MEMFS.node_ops.setattr - }, - stream: { - llseek: MEMFS.stream_ops.llseek, - read: MEMFS.stream_ops.read, - write: MEMFS.stream_ops.write, - allocate: MEMFS.stream_ops.allocate, - mmap: MEMFS.stream_ops.mmap, - msync: MEMFS.stream_ops.msync - } + MEMFS.ops_table ||= { + dir: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr, + lookup: MEMFS.node_ops.lookup, + mknod: MEMFS.node_ops.mknod, + rename: MEMFS.node_ops.rename, + unlink: MEMFS.node_ops.unlink, + rmdir: MEMFS.node_ops.rmdir, + readdir: MEMFS.node_ops.readdir, + symlink: MEMFS.node_ops.symlink }, - link: { - node: { - getattr: MEMFS.node_ops.getattr, - setattr: MEMFS.node_ops.setattr, - readlink: MEMFS.node_ops.readlink - }, - stream: {} + stream: { + llseek: MEMFS.stream_ops.llseek + } + }, + file: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr }, - chrdev: { - node: { - getattr: MEMFS.node_ops.getattr, - setattr: MEMFS.node_ops.setattr - }, - stream: FS.chrdev_stream_ops + stream: { + llseek: MEMFS.stream_ops.llseek, + read: MEMFS.stream_ops.read, + write: MEMFS.stream_ops.write, + mmap: MEMFS.stream_ops.mmap, + msync: MEMFS.stream_ops.msync } - }; - } + }, + link: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr, + readlink: MEMFS.node_ops.readlink + }, + stream: {} + }, + chrdev: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr + }, + stream: FS.chrdev_stream_ops + } + }; var node = FS.createNode(parent, name, mode, dev); if (FS.isDir(node.mode)) { node.node_ops = MEMFS.ops_table.dir.node; @@ -1637,11 +1437,14 @@ function dbg(text) { } else if (FS.isFile(node.mode)) { node.node_ops = MEMFS.ops_table.file.node; node.stream_ops = MEMFS.ops_table.file.stream; - node.usedBytes = 0; // The actual number of bytes used in the typed array, as opposed to contents.length which gives the whole capacity. - // When the byte data of the file is populated, this will point to either a typed array, or a normal JS array. Typed arrays are preferred - // for performance, and used by default. However, typed arrays are not resizable like normal JS arrays are, so there is a small disk size - // penalty involved for appending file writes that continuously grow a file similar to std::vector capacity vs used -scheme. - node.contents = null; + // The actual number of bytes used in the typed array, as opposed to + // contents.length which gives the whole capacity. + node.usedBytes = 0; + // The byte data of the file is stored in a typed array. + // Note: typed arrays are not resizable like normal JS arrays are, so + // there is a small penalty involved for appending file writes that + // continuously grow a file similar to std::vector capacity vs used. + node.contents = MEMFS.emptyFileContents ??= new Uint8Array(0); } else if (FS.isLink(node.mode)) { node.node_ops = MEMFS.ops_table.link.node; node.stream_ops = MEMFS.ops_table.link.stream; @@ -1649,45 +1452,39 @@ function dbg(text) { node.node_ops = MEMFS.ops_table.chrdev.node; node.stream_ops = MEMFS.ops_table.chrdev.stream; } - node.timestamp = Date.now(); + node.atime = node.mtime = node.ctime = Date.now(); // add the new node to the parent if (parent) { parent.contents[name] = node; - parent.timestamp = node.timestamp; + parent.atime = parent.mtime = parent.ctime = node.atime; } return node; }, getFileDataAsTypedArray(node) { - if (!node.contents) return new Uint8Array(0); - if (node.contents.subarray) return node.contents.subarray(0, node.usedBytes); // Make sure to not return excess unused bytes. - return new Uint8Array(node.contents); + assert(FS.isFile(node.mode), 'getFileDataAsTypedArray called on non-file'); + return node.contents.subarray(0, node.usedBytes); // Make sure to not return excess unused bytes. }, expandFileStorage(node, newCapacity) { - var prevCapacity = node.contents ? node.contents.length : 0; + var prevCapacity = node.contents.length; if (prevCapacity >= newCapacity) return; // No need to expand, the storage was already large enough. - // Don't expand strictly to the given requested limit if it's only a very small increase, but instead geometrically grow capacity. - // For small filesizes (<1MB), perform size*2 geometric increase, but for large sizes, do a much more conservative size*1.125 increase to - // avoid overshooting the allocation cap by a very large margin. + // Don't expand strictly to the given requested limit if it's only a very + // small increase, but instead geometrically grow capacity. + // For small filesizes (<1MB), perform size*2 geometric increase, but for + // large sizes, do a much more conservative size*1.125 increase to avoid + // overshooting the allocation cap by a very large margin. var CAPACITY_DOUBLING_MAX = 1024 * 1024; newCapacity = Math.max(newCapacity, (prevCapacity * (prevCapacity < CAPACITY_DOUBLING_MAX ? 2.0 : 1.125)) >>> 0); - if (prevCapacity != 0) newCapacity = Math.max(newCapacity, 256); // At minimum allocate 256b for each file when expanding. - var oldContents = node.contents; + if (prevCapacity) newCapacity = Math.max(newCapacity, 256); // At minimum allocate 256b for each file when expanding. + var oldContents = MEMFS.getFileDataAsTypedArray(node); node.contents = new Uint8Array(newCapacity); // Allocate new storage. - if (node.usedBytes > 0) node.contents.set(oldContents.subarray(0, node.usedBytes), 0); // Copy old data over to the new storage. + node.contents.set(oldContents); }, resizeFileStorage(node, newSize) { if (node.usedBytes == newSize) return; - if (newSize == 0) { - node.contents = null; // Fully decommit when requesting a resize to zero. - node.usedBytes = 0; - } else { - var oldContents = node.contents; - node.contents = new Uint8Array(newSize); // Allocate new storage. - if (oldContents) { - node.contents.set(oldContents.subarray(0, Math.min(newSize, node.usedBytes))); // Copy old data over to the new storage. - } - node.usedBytes = newSize; - } + var oldContents = node.contents; + node.contents = new Uint8Array(newSize); // Allocate new storage. + node.contents.set(oldContents.subarray(0, Math.min(newSize, node.usedBytes))); // Copy old data over to the new storage. + node.usedBytes = newSize; }, node_ops:{ getattr(node) { @@ -1709,9 +1506,9 @@ function dbg(text) { } else { attr.size = 0; } - attr.atime = new Date(node.timestamp); - attr.mtime = new Date(node.timestamp); - attr.ctime = new Date(node.timestamp); + attr.atime = new Date(node.atime); + attr.mtime = new Date(node.mtime); + attr.ctime = new Date(node.ctime); // NOTE: In our implementation, st_blocks = Math.ceil(st_size/st_blksize), // but this is not required by the standard. attr.blksize = 4096; @@ -1719,47 +1516,44 @@ function dbg(text) { return attr; }, setattr(node, attr) { - if (attr.mode !== undefined) { - node.mode = attr.mode; - } - if (attr.timestamp !== undefined) { - node.timestamp = attr.timestamp; + for (const key of ["mode", "atime", "mtime", "ctime"]) { + if (attr[key] != null) { + node[key] = attr[key]; + } } if (attr.size !== undefined) { MEMFS.resizeFileStorage(node, attr.size); } }, lookup(parent, name) { - throw FS.genericErrors[44]; + throw new FS.ErrnoError(44); }, mknod(parent, name, mode, dev) { return MEMFS.createNode(parent, name, mode, dev); }, rename(old_node, new_dir, new_name) { - // if we're overwriting a directory at new_name, make sure it's empty. - if (FS.isDir(old_node.mode)) { - var new_node; - try { - new_node = FS.lookupNode(new_dir, new_name); - } catch (e) { - } - if (new_node) { + var new_node; + try { + new_node = FS.lookupNode(new_dir, new_name); + } catch (e) {} + if (new_node) { + if (FS.isDir(old_node.mode)) { + // if we're overwriting a directory at new_name, make sure it's empty. for (var i in new_node.contents) { throw new FS.ErrnoError(55); } } + FS.hashRemoveNode(new_node); } // do the internal rewiring delete old_node.parent.contents[old_node.name]; - old_node.parent.timestamp = Date.now() - old_node.name = new_name; new_dir.contents[new_name] = old_node; - new_dir.timestamp = old_node.parent.timestamp; - old_node.parent = new_dir; + old_node.name = new_name; + new_dir.ctime = new_dir.mtime = old_node.parent.ctime = old_node.parent.mtime = Date.now(); }, unlink(parent, name) { delete parent.contents[name]; - parent.timestamp = Date.now(); + parent.ctime = parent.mtime = Date.now(); }, rmdir(parent, name) { var node = FS.lookupNode(parent, name); @@ -1767,20 +1561,13 @@ function dbg(text) { throw new FS.ErrnoError(55); } delete parent.contents[name]; - parent.timestamp = Date.now(); + parent.ctime = parent.mtime = Date.now(); }, readdir(node) { - var entries = ['.', '..']; - for (var key in node.contents) { - if (!node.contents.hasOwnProperty(key)) { - continue; - } - entries.push(key); - } - return entries; + return ['.', '..', ...Object.keys(node.contents)]; }, symlink(parent, newname, oldpath) { - var node = MEMFS.createNode(parent, newname, 511 /* 0777 */ | 40960, 0); + var node = MEMFS.createNode(parent, newname, 0o777 | 40960, 0); node.link = oldpath; return node; }, @@ -1797,48 +1584,29 @@ function dbg(text) { if (position >= stream.node.usedBytes) return 0; var size = Math.min(stream.node.usedBytes - position, length); assert(size >= 0); - if (size > 8 && contents.subarray) { // non-trivial, and typed array - buffer.set(contents.subarray(position, position + size), offset); - } else { - for (var i = 0; i < size; i++) buffer[offset + i] = contents[position + i]; - } + buffer.set(contents.subarray(position, position + size), offset); return size; }, write(stream, buffer, offset, length, position, canOwn) { - // The data buffer should be a typed array view - assert(!(buffer instanceof ArrayBuffer)); - + assert(buffer.subarray, 'FS.write expects a TypedArray'); + if (!length) return 0; var node = stream.node; - node.timestamp = Date.now(); - - if (buffer.subarray && (!node.contents || node.contents.subarray)) { // This write is from a typed array to a typed array? - if (canOwn) { - assert(position === 0, 'canOwn must imply no weird position inside the file'); - node.contents = buffer.subarray(offset, offset + length); - node.usedBytes = length; - return length; - } else if (node.usedBytes === 0 && position === 0) { // If this is a simple first write to an empty file, do a fast set since we don't need to care about old data. - node.contents = buffer.slice(offset, offset + length); - node.usedBytes = length; - return length; - } else if (position + length <= node.usedBytes) { // Writing to an already allocated and used subrange of the file? - node.contents.set(buffer.subarray(offset, offset + length), position); - return length; - } - } - - // Appending to an existing file and we need to reallocate, or source data did not come as a typed array. - MEMFS.expandFileStorage(node, position+length); - if (node.contents.subarray && buffer.subarray) { + node.mtime = node.ctime = Date.now(); + + if (canOwn) { + assert(position === 0, 'canOwn must imply no weird position inside the file'); + node.contents = buffer.subarray(offset, offset + length); + node.usedBytes = length; + } else if (node.usedBytes === 0 && position === 0) { // If this is a simple first write to an empty file, do a fast set since we don't need to care about old data. + node.contents = buffer.slice(offset, offset + length); + node.usedBytes = length; + } else { + MEMFS.expandFileStorage(node, position+length); // Use typed array write which is available. node.contents.set(buffer.subarray(offset, offset + length), position); - } else { - for (var i = 0; i < length; i++) { - node.contents[position + i] = buffer[offset + i]; // Or fall back to manual write if not. - } + node.usedBytes = Math.max(node.usedBytes, position + length); } - node.usedBytes = Math.max(node.usedBytes, position + length); return length; }, llseek(stream, offset, whence) { @@ -1855,10 +1623,6 @@ function dbg(text) { } return position; }, - allocate(stream, offset, length) { - MEMFS.expandFileStorage(stream.node, offset + length); - stream.node.usedBytes = Math.max(stream.node.usedBytes, offset + length); - }, mmap(stream, length, position, prot, flags) { if (!FS.isFile(stream.node.mode)) { throw new FS.ErrnoError(43); @@ -1873,20 +1637,22 @@ function dbg(text) { allocated = false; ptr = contents.byteOffset; } else { - // Try to avoid unnecessary slices. - if (position > 0 || position + length < contents.length) { - if (contents.subarray) { - contents = contents.subarray(position, position + length); - } else { - contents = Array.prototype.slice.call(contents, position, position + length); - } - } allocated = true; ptr = mmapAlloc(length); if (!ptr) { throw new FS.ErrnoError(48); } - HEAP8.set(contents, ptr); + if (contents) { + // Try to avoid unnecessary slices. + if (position > 0 || position + length < contents.length) { + if (contents.subarray) { + contents = contents.subarray(position, position + length); + } else { + contents = Array.prototype.slice.call(contents, position, position + length); + } + } + HEAP8.set(contents, ptr); + } } return { ptr, allocated }; }, @@ -1897,75 +1663,9 @@ function dbg(text) { }, }, }; - - /** @param {boolean=} noRunDep */ - var asyncLoad = (url, onload, onerror, noRunDep) => { - var dep = !noRunDep ? getUniqueRunDependency(`al ${url}`) : ''; - readAsync(url, (arrayBuffer) => { - assert(arrayBuffer, `Loading data file "${url}" failed (no arrayBuffer).`); - onload(new Uint8Array(arrayBuffer)); - if (dep) removeRunDependency(dep); - }, (event) => { - if (onerror) { - onerror(); - } else { - throw `Loading data file "${url}" failed.`; - } - }); - if (dep) addRunDependency(dep); - }; - - - var FS_createDataFile = (parent, name, fileData, canRead, canWrite, canOwn) => { - FS.createDataFile(parent, name, fileData, canRead, canWrite, canOwn); - }; - - var preloadPlugins = Module['preloadPlugins'] || []; - var FS_handledByPreloadPlugin = (byteArray, fullname, finish, onerror) => { - // Ensure plugins are ready. - if (typeof Browser != 'undefined') Browser.init(); - - var handled = false; - preloadPlugins.forEach((plugin) => { - if (handled) return; - if (plugin['canHandle'](fullname)) { - plugin['handle'](byteArray, fullname, finish, onerror); - handled = true; - } - }); - return handled; - }; - var FS_createPreloadedFile = (parent, name, url, canRead, canWrite, onload, onerror, dontCreateFile, canOwn, preFinish) => { - // TODO we should allow people to just pass in a complete filename instead - // of parent and name being that we just join them anyways - var fullname = name ? PATH_FS.resolve(PATH.join2(parent, name)) : parent; - var dep = getUniqueRunDependency(`cp ${fullname}`); // might have several active requests for the same fullname - function processData(byteArray) { - function finish(byteArray) { - if (preFinish) preFinish(); - if (!dontCreateFile) { - FS_createDataFile(parent, name, byteArray, canRead, canWrite, canOwn); - } - if (onload) onload(); - removeRunDependency(dep); - } - if (FS_handledByPreloadPlugin(byteArray, fullname, finish, () => { - if (onerror) onerror(); - removeRunDependency(dep); - })) { - return; - } - finish(byteArray); - } - addRunDependency(dep); - if (typeof url == 'string') { - asyncLoad(url, (byteArray) => processData(byteArray), onerror); - } else { - processData(url); - } - }; - + var FS_modeStringToFlags = (str) => { + if (typeof str != 'string') return str; var flagModes = { 'r': 0, 'r+': 2, @@ -1980,19 +1680,27 @@ function dbg(text) { } return flags; }; - + + var FS_fileDataToTypedArray = (data) => { + if (typeof data == 'string') { + data = intArrayFromString(data, true); + } + if (!data.subarray) { + data = new Uint8Array(data); + } + return data; + }; + var FS_getMode = (canRead, canWrite) => { var mode = 0; if (canRead) mode |= 292 | 73; if (canWrite) mode |= 146; return mode; }; - - - - - - + + + + var ERRNO_CODES = { 'EPERM': 63, 'ENOENT': 44, @@ -2116,16 +1824,12 @@ function dbg(text) { 'EOWNERDEAD': 62, 'ESTRPIPE': 135, }; - + var NODEFS = { isWindows:false, staticInit() { NODEFS.isWindows = !!process.platform.match(/^win/); - var flags = process.binding("constants"); - // Node.js 4 compatibility: it has no namespaces for constants - if (flags["fs"]) { - flags = flags["fs"]; - } + var flags = process.binding("constants")["fs"]; NODEFS.flagsForNodeMap = { "1024": flags["O_APPEND"], "64": flags["O_CREAT"], @@ -2147,6 +1851,17 @@ function dbg(text) { assert(code in ERRNO_CODES, `unexpected node error code: ${code} (${e})`); return ERRNO_CODES[code]; }, + tryFSOperation(f) { + try { + return f(); + } catch (e) { + if (!e.code) throw e; + // node under windows can return code 'UNKNOWN' here: + // https://github.com/emscripten-core/emscripten/issues/15468 + if (e.code === 'UNKNOWN') throw new FS.ErrnoError(28); + throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); + } + }, mount(mount) { assert(ENVIRONMENT_IS_NODE); return NODEFS.createNode(null, '/', NODEFS.getMode(mount.opts.root), 0); @@ -2161,19 +1876,15 @@ function dbg(text) { return node; }, getMode(path) { - var stat; - try { - stat = fs.lstatSync(path); + return NODEFS.tryFSOperation(() => { + var mode = fs.lstatSync(path).mode; if (NODEFS.isWindows) { - // Node.js on Windows never represents permission bit 'x', so - // propagate read bits to execute bits - stat.mode = stat.mode | ((stat.mode & 292) >> 2); + // Windows does not report the 'x' permission bit, so propagate read + // bits to execute bits. + mode |= (mode & 292) >> 2; } - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } - return stat.mode; + return mode; + }); }, realPath(node) { var parts = []; @@ -2183,7 +1894,7 @@ function dbg(text) { } parts.push(node.mount.opts.root); parts.reverse(); - return PATH.join.apply(null, parts); + return PATH.join(...parts); }, flagsForNode(flags) { flags &= ~2097152; // Ignore this flag from musl, otherwise node.js fails to open the file. @@ -2203,59 +1914,77 @@ function dbg(text) { } return newFlags; }, - node_ops:{ - getattr(node) { - var path = NODEFS.realPath(node); - var stat; - try { - stat = fs.lstatSync(path); - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } - // node.js v0.10.20 doesn't report blksize and blocks on Windows. Fake them with default blksize of 4096. + getattr(func, node) { + var stat = NODEFS.tryFSOperation(func); + if (NODEFS.isWindows) { + // node.js v0.10.20 doesn't report blksize and blocks on Windows. Fake + // them with default blksize of 4096. // See http://support.microsoft.com/kb/140365 - if (NODEFS.isWindows && !stat.blksize) { + if (!stat.blksize) { stat.blksize = 4096; } - if (NODEFS.isWindows && !stat.blocks) { + if (!stat.blocks) { stat.blocks = (stat.size+stat.blksize-1)/stat.blksize|0; } - return { - dev: stat.dev, - ino: stat.ino, - mode: stat.mode, - nlink: stat.nlink, - uid: stat.uid, - gid: stat.gid, - rdev: stat.rdev, - size: stat.size, - atime: stat.atime, - mtime: stat.mtime, - ctime: stat.ctime, - blksize: stat.blksize, - blocks: stat.blocks - }; + // Windows does not report the 'x' permission bit, so propagate read + // bits to execute bits. + stat.mode |= (stat.mode & 292) >> 2; + } + return { + dev: stat.dev, + ino: node.id, + mode: stat.mode, + nlink: stat.nlink, + uid: stat.uid, + gid: stat.gid, + rdev: stat.rdev, + size: stat.size, + atime: stat.atime, + mtime: stat.mtime, + ctime: stat.ctime, + blksize: stat.blksize, + blocks: stat.blocks + }; + }, + setattr(arg, node, attr, chmod, utimes, truncate, stat) { + NODEFS.tryFSOperation(() => { + if (attr.mode !== undefined) { + var mode = attr.mode; + if (NODEFS.isWindows) { + // Windows only supports S_IREAD / S_IWRITE (S_IRUSR / S_IWUSR) + // https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/chmod-wchmod + mode &= 384; + } + chmod(arg, mode); + // update the common node structure mode as well + node.mode = attr.mode; + } + if (typeof (attr.atime ?? attr.mtime) === "number") { + // Unfortunately, we have to stat the current value if we don't want + // to change it. On top of that, since the times don't round trip + // this will only keep the value nearly unchanged not exactly + // unchanged. See: + // https://github.com/nodejs/node/issues/56492 + var atime = new Date(attr.atime ?? stat(arg).atime); + var mtime = new Date(attr.mtime ?? stat(arg).mtime); + utimes(arg, atime, mtime); + } + if (attr.size !== undefined) { + truncate(arg, attr.size); + } + }); + }, + node_ops:{ + getattr(node) { + var path = NODEFS.realPath(node); + return NODEFS.getattr(() => fs.lstatSync(path), node); }, setattr(node, attr) { var path = NODEFS.realPath(node); - try { - if (attr.mode !== undefined) { - fs.chmodSync(path, attr.mode); - // update the common node structure mode as well - node.mode = attr.mode; - } - if (attr.timestamp !== undefined) { - var date = new Date(attr.timestamp); - fs.utimesSync(path, date, date); - } - if (attr.size !== undefined) { - fs.truncateSync(path, attr.size); - } - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); + if (attr.mode != null && attr.dontFollow) { + throw new FS.ErrnoError(52); } + NODEFS.setattr(path, node, attr, fs.chmodSync, fs.utimesSync, fs.truncateSync, fs.lstatSync); }, lookup(parent, name) { var path = PATH.join2(NODEFS.realPath(parent), name); @@ -2266,117 +1995,85 @@ function dbg(text) { var node = NODEFS.createNode(parent, name, mode, dev); // create the backing node for this in the fs root as well var path = NODEFS.realPath(node); - try { + NODEFS.tryFSOperation(() => { if (FS.isDir(node.mode)) { fs.mkdirSync(path, node.mode); } else { fs.writeFileSync(path, '', { mode: node.mode }); } - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + }); return node; }, rename(oldNode, newDir, newName) { var oldPath = NODEFS.realPath(oldNode); var newPath = PATH.join2(NODEFS.realPath(newDir), newName); try { - fs.renameSync(oldPath, newPath); - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + FS.unlink(newPath); + } catch(e) {} + NODEFS.tryFSOperation(() => fs.renameSync(oldPath, newPath)); oldNode.name = newName; }, unlink(parent, name) { var path = PATH.join2(NODEFS.realPath(parent), name); - try { - fs.unlinkSync(path); - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + NODEFS.tryFSOperation(() => fs.unlinkSync(path)); }, rmdir(parent, name) { var path = PATH.join2(NODEFS.realPath(parent), name); - try { - fs.rmdirSync(path); - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + NODEFS.tryFSOperation(() => fs.rmdirSync(path)); }, readdir(node) { var path = NODEFS.realPath(node); - try { - return fs.readdirSync(path); - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + return NODEFS.tryFSOperation(() => fs.readdirSync(path)); }, symlink(parent, newName, oldPath) { var newPath = PATH.join2(NODEFS.realPath(parent), newName); - try { - fs.symlinkSync(oldPath, newPath); - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + NODEFS.tryFSOperation(() => fs.symlinkSync(oldPath, newPath)); }, readlink(node) { var path = NODEFS.realPath(node); - try { - path = fs.readlinkSync(path); - path = nodePath.relative(nodePath.resolve(node.mount.opts.root), path); - return path; - } catch (e) { - if (!e.code) throw e; - // node under windows can return code 'UNKNOWN' here: - // https://github.com/emscripten-core/emscripten/issues/15468 - if (e.code === 'UNKNOWN') throw new FS.ErrnoError(28); - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + return NODEFS.tryFSOperation(() => fs.readlinkSync(path)); + }, + statfs(path) { + var stats = NODEFS.tryFSOperation(() => fs.statfsSync(path)); + // Node.js doesn't provide frsize (fragment size). Set it to bsize (block size) + // as they're often the same in many file systems. May not be accurate for all. + stats.frsize = stats.bsize; + return stats; }, }, stream_ops:{ + getattr(stream) { + return NODEFS.getattr(() => fs.fstatSync(stream.nfd), stream.node); + }, + setattr(stream, attr) { + NODEFS.setattr(stream.nfd, stream.node, attr, fs.fchmodSync, fs.futimesSync, fs.ftruncateSync, fs.fstatSync); + }, open(stream) { var path = NODEFS.realPath(stream.node); - try { - if (FS.isFile(stream.node.mode)) { - stream.nfd = fs.openSync(path, NODEFS.flagsForNode(stream.flags)); - } - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + NODEFS.tryFSOperation(() => { + stream.shared.refcount = 1; + stream.nfd = fs.openSync(path, NODEFS.flagsForNode(stream.flags)); + }); }, close(stream) { - try { - if (FS.isFile(stream.node.mode) && stream.nfd) { + NODEFS.tryFSOperation(() => { + if (stream.nfd && --stream.shared.refcount === 0) { fs.closeSync(stream.nfd); } - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + }); + }, + dup(stream) { + stream.shared.refcount++; }, read(stream, buffer, offset, length, position) { - // Node.js < 6 compatibility: node errors on 0 length reads - if (length === 0) return 0; - try { - return fs.readSync(stream.nfd, new Int8Array(buffer.buffer, offset, length), { position: position }); - } catch (e) { - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + return NODEFS.tryFSOperation(() => + fs.readSync(stream.nfd, buffer, offset, length, position) + ); }, write(stream, buffer, offset, length, position) { - try { - return fs.writeSync(stream.nfd, new Int8Array(buffer.buffer, offset, length), { position: position }); - } catch (e) { - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + return NODEFS.tryFSOperation(() => + fs.writeSync(stream.nfd, buffer, offset, length, position) + ); }, llseek(stream, offset, whence) { var position = offset; @@ -2384,28 +2081,26 @@ function dbg(text) { position += stream.position; } else if (whence === 2) { if (FS.isFile(stream.node.mode)) { - try { + NODEFS.tryFSOperation(() => { var stat = fs.fstatSync(stream.nfd); position += stat.size; - } catch (e) { - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + }); } } - + if (position < 0) { throw new FS.ErrnoError(28); } - + return position; }, mmap(stream, length, position, prot, flags) { if (!FS.isFile(stream.node.mode)) { throw new FS.ErrnoError(43); } - + var ptr = mmapAlloc(length); - + NODEFS.stream_ops.read(stream, HEAP8, ptr, length, position); return { ptr, allocated: true }; }, @@ -2416,11 +2111,75 @@ function dbg(text) { }, }, }; - - - - - + + + + + + + var NODERAWFS_stream_funcs = { + close(stream) { + VFS.closeStream(stream.fd); + // Don't close stdin/stdout/stderr since they are used by node itself. + if (--stream.shared.refcnt <= 0 && stream.nfd > 2) { + // This stream is created by our Node.js filesystem, close the + // native file descriptor when its reference count drops to 0. + fs.closeSync(stream.nfd); + } + }, + llseek(stream, offset, whence) { + var position = offset; + if (whence === 1) { + position += stream.position; + } else if (whence === 2) { + position += fs.fstatSync(stream.nfd).size; + } else if (whence !== 0) { + throw new FS.ErrnoError(28); + } + + if (position < 0) { + throw new FS.ErrnoError(28); + } + stream.position = position; + return position; + }, + read(stream, buffer, offset, length, position) { + var seeking = typeof position != 'undefined'; + if (!seeking && stream.seekable) position = stream.position; + var bytesRead = fs.readSync(stream.nfd, buffer, offset, length, position); + // update position marker when non-seeking + if (!seeking) stream.position += bytesRead; + return bytesRead; + }, + write(stream, buffer, offset, length, position) { + if (stream.flags & 1024) { + // seek to the end before writing in append mode + FS.llseek(stream, 0, 2); + } + var seeking = typeof position != 'undefined'; + if (!seeking && stream.seekable) position = stream.position; + var bytesWritten = fs.writeSync(stream.nfd, buffer, offset, length, position); + // update position marker when non-seeking + if (!seeking) stream.position += bytesWritten; + return bytesWritten; + }, + mmap(stream, length, position, prot, flags) { + if (!length) { + throw new FS.ErrnoError(28); + } + var ptr = mmapAlloc(length); + FS.read(stream, HEAP8, ptr, length, position); + return { ptr, allocated: true }; + }, + msync(stream, buffer, offset, length, mmapFlags) { + FS.write(stream, buffer, 0, length, offset); + // should we check if bytesWritten and length are the same? + return 0; + }, + ioctl(stream, cmd, arg) { + throw new FS.ErrnoError(59); + }, + }; var NODERAWFS = { lookup(parent, name) { assert(parent) @@ -2429,20 +2188,21 @@ function dbg(text) { }, lookupPath(path, opts = {}) { if (opts.parent) { - path = nodePath.dirname(path); + path = PATH.dirname(path); } var st = fs.lstatSync(path); var mode = NODEFS.getMode(path); return { path, node: { id: st.ino, mode, node_ops: NODERAWFS, path }}; }, createStandardStreams() { - FS.createStream({ nfd: 0, position: 0, path: '', flags: 0, tty: true, seekable: false }, 0); + FS.createStream({ nfd: 0, position: 0, path: '/dev/stdin', flags: 0, seekable: false }, 0); + var paths = [,'/dev/stdout', '/dev/stderr']; for (var i = 1; i < 3; i++) { - FS.createStream({ nfd: i, position: 0, path: '', flags: 577, tty: true, seekable: false }, i); + FS.createStream({ nfd: i, position: 0, path: paths[i], flags: 577, seekable: false }, i); } }, cwd() { return process.cwd(); }, - chdir() { process.chdir.apply(void 0, arguments); }, + chdir(...args) { process.chdir(...args); }, mknod(path, mode) { if (FS.isDir(path)) { fs.mkdirSync(path, mode); @@ -2450,26 +2210,69 @@ function dbg(text) { fs.writeFileSync(path, '', { mode: mode }); } }, - mkdir() { fs.mkdirSync.apply(void 0, arguments); }, - symlink() { fs.symlinkSync.apply(void 0, arguments); }, - rename() { fs.renameSync.apply(void 0, arguments); }, - rmdir() { fs.rmdirSync.apply(void 0, arguments); }, - readdir() { return ['.', '..'].concat(fs.readdirSync.apply(void 0, arguments)); }, - unlink() { fs.unlinkSync.apply(void 0, arguments); }, - readlink() { return fs.readlinkSync.apply(void 0, arguments); }, - stat() { return fs.statSync.apply(void 0, arguments); }, - lstat() { return fs.lstatSync.apply(void 0, arguments); }, - chmod() { fs.chmodSync.apply(void 0, arguments); }, + mkdir(...args) { fs.mkdirSync(...args); }, + symlink(...args) { fs.symlinkSync(...args); }, + rename(...args) { fs.renameSync(...args); }, + rmdir(...args) { fs.rmdirSync(...args); }, + readdir(...args) { return ['.', '..'].concat(fs.readdirSync(...args)); }, + unlink(...args) { fs.unlinkSync(...args); }, + readlink(...args) { return fs.readlinkSync(...args); }, + stat(path, dontFollow) { + var stat = dontFollow ? fs.lstatSync(path) : fs.statSync(path); + if (NODEFS.isWindows) { + // Windows does not report the 'x' permission bit, so propagate read + // bits to execute bits. + stat.mode |= (stat.mode & 292) >> 2; + } + return stat; + }, + fstat(fd) { + var stream = FS.getStreamChecked(fd); + return fs.fstatSync(stream.nfd); + }, + statfs(path) { + // Node's fs.statfsSync API doesn't provide these attributes so include + // some defaults. + var defaults = { + fsid: 42, + flags: 2, + namelen: 255, + } + return Object.assign(defaults, fs.statfsSync(path)); + }, + statfsStream(stream) { + return FS.statfs(stream.path); + }, + chmod(path, mode, dontFollow) { + mode &= 4095; + if (NODEFS.isWindows) { + // Windows only supports S_IREAD / S_IWRITE (S_IRUSR / S_IWUSR) + // https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/chmod-wchmod + mode &= 384; + } + if (dontFollow && fs.lstatSync(path).isSymbolicLink()) { + // Node (and indeed linux) does not support chmod on symlinks + // https://nodejs.org/api/fs.html#fslchmodsyncpath-mode + throw new FS.ErrnoError(138); + } + fs.chmodSync(path, mode); + }, fchmod(fd, mode) { var stream = FS.getStreamChecked(fd); fs.fchmodSync(stream.nfd, mode); }, - chown() { fs.chownSync.apply(void 0, arguments); }, + chown(...args) { fs.chownSync(...args); }, fchown(fd, owner, group) { var stream = FS.getStreamChecked(fd); fs.fchownSync(stream.nfd, owner, group); }, - truncate() { fs.truncateSync.apply(void 0, arguments); }, + truncate(path, len) { + // See https://github.com/nodejs/node/issues/35632 + if (len < 0) { + throw new FS.ErrnoError(28); + } + return fs.truncateSync(path, len); + }, ftruncate(fd, len) { // See https://github.com/nodejs/node/issues/35632 if (len < 0) { @@ -2478,12 +2281,20 @@ function dbg(text) { var stream = FS.getStreamChecked(fd); fs.ftruncateSync(stream.nfd, len); }, - utime(path, atime, mtime) { fs.utimesSync(path, atime/1000, mtime/1000); }, - open(path, flags, mode) { - if (typeof flags == "string") { - flags = FS_modeStringToFlags(flags) + utime(path, atime, mtime) { + // null here for atime or mtime means UTIME_OMIT was passed. Since node + // doesn't support this concept we need to first find the existing + // timestamps in order to preserve them. + if ((atime === null) || (mtime === null)) { + var st = fs.statSync(path); + atime ||= st.atimeMs; + mtime ||= st.mtimeMs; } - var pathTruncated = path.split('/').map(function(s) { return s.substr(0, 255); }).join('/'); + fs.utimesSync(path, atime/1000, mtime/1000); + }, + open(path, flags, mode) { + flags = FS_modeStringToFlags(flags); + var pathTruncated = path.split('/').map((s) => s.slice(0, 255)).join('/'); var nfd = fs.openSync(pathTruncated, NODEFS.flagsForNode(flags), mode); var st = fs.fstatSync(nfd); if (flags & 65536 && !st.isDirectory()) { @@ -2497,235 +2308,97 @@ function dbg(text) { createStream(stream, fd) { // Call the original FS.createStream var rtn = VFS.createStream(stream, fd); - if (typeof rtn.shared.refcnt == 'undefined') { - rtn.shared.refcnt = 1; - } else { + // Detect PIPEFS streams and skip the refcnt/tty initialization in that case. + if (!stream.stream_ops) { + rtn.shared.refcnt ??= 0; rtn.shared.refcnt++; + rtn.tty = nodeTTY.isatty(rtn.nfd); } return rtn; }, - close(stream) { - VFS.closeStream(stream.fd); - if (!stream.stream_ops && --stream.shared.refcnt === 0) { - // This stream is created by our Node.js filesystem, close the - // native file descriptor when its reference count drops to 0. - fs.closeSync(stream.nfd); - } - }, - llseek(stream, offset, whence) { - if (stream.stream_ops) { - // this stream is created by in-memory filesystem - return VFS.llseek(stream, offset, whence); - } - var position = offset; - if (whence === 1) { - position += stream.position; - } else if (whence === 2) { - position += fs.fstatSync(stream.nfd).size; - } else if (whence !== 0) { - throw new FS.ErrnoError(28); - } - - if (position < 0) { - throw new FS.ErrnoError(28); - } - stream.position = position; - return position; - }, - read(stream, buffer, offset, length, position) { - if (stream.stream_ops) { - // this stream is created by in-memory filesystem - return VFS.read(stream, buffer, offset, length, position); - } - var seeking = typeof position != 'undefined'; - if (!seeking && stream.seekable) position = stream.position; - var bytesRead = fs.readSync(stream.nfd, new Int8Array(buffer.buffer, offset, length), { position: position }); - // update position marker when non-seeking - if (!seeking) stream.position += bytesRead; - return bytesRead; - }, - write(stream, buffer, offset, length, position) { - if (stream.stream_ops) { - // this stream is created by in-memory filesystem - return VFS.write(stream, buffer, offset, length, position); - } - if (stream.flags & +"1024") { - // seek to the end before writing in append mode - FS.llseek(stream, 0, +"2"); - } - var seeking = typeof position != 'undefined'; - if (!seeking && stream.seekable) position = stream.position; - var bytesWritten = fs.writeSync(stream.nfd, new Int8Array(buffer.buffer, offset, length), { position: position }); - // update position marker when non-seeking - if (!seeking) stream.position += bytesWritten; - return bytesWritten; - }, - allocate() { - throw new FS.ErrnoError(138); - }, - mmap(stream, length, position, prot, flags) { - if (stream.stream_ops) { - // this stream is created by in-memory filesystem - return VFS.mmap(stream, length, position, prot, flags); - } - - var ptr = mmapAlloc(length); - FS.read(stream, HEAP8, ptr, length, position); - return { ptr, allocated: true }; - }, - msync(stream, buffer, offset, length, mmapFlags) { - if (stream.stream_ops) { - // this stream is created by in-memory filesystem - return VFS.msync(stream, buffer, offset, length, mmapFlags); - } - - FS.write(stream, buffer, 0, length, offset); - // should we check if bytesWritten and length are the same? - return 0; - }, - munmap() { - return 0; - }, - ioctl() { - throw new FS.ErrnoError(59); - }, - }; - - var ERRNO_MESSAGES = { - 0:"Success", - 1:"Arg list too long", - 2:"Permission denied", - 3:"Address already in use", - 4:"Address not available", - 5:"Address family not supported by protocol family", - 6:"No more processes", - 7:"Socket already connected", - 8:"Bad file number", - 9:"Trying to read unreadable message", - 10:"Mount device busy", - 11:"Operation canceled", - 12:"No children", - 13:"Connection aborted", - 14:"Connection refused", - 15:"Connection reset by peer", - 16:"File locking deadlock error", - 17:"Destination address required", - 18:"Math arg out of domain of func", - 19:"Quota exceeded", - 20:"File exists", - 21:"Bad address", - 22:"File too large", - 23:"Host is unreachable", - 24:"Identifier removed", - 25:"Illegal byte sequence", - 26:"Connection already in progress", - 27:"Interrupted system call", - 28:"Invalid argument", - 29:"I/O error", - 30:"Socket is already connected", - 31:"Is a directory", - 32:"Too many symbolic links", - 33:"Too many open files", - 34:"Too many links", - 35:"Message too long", - 36:"Multihop attempted", - 37:"File or path name too long", - 38:"Network interface is not configured", - 39:"Connection reset by network", - 40:"Network is unreachable", - 41:"Too many open files in system", - 42:"No buffer space available", - 43:"No such device", - 44:"No such file or directory", - 45:"Exec format error", - 46:"No record locks available", - 47:"The link has been severed", - 48:"Not enough core", - 49:"No message of desired type", - 50:"Protocol not available", - 51:"No space left on device", - 52:"Function not implemented", - 53:"Socket is not connected", - 54:"Not a directory", - 55:"Directory not empty", - 56:"State not recoverable", - 57:"Socket operation on non-socket", - 59:"Not a typewriter", - 60:"No such device or address", - 61:"Value too large for defined data type", - 62:"Previous owner died", - 63:"Not super-user", - 64:"Broken pipe", - 65:"Protocol error", - 66:"Unknown protocol", - 67:"Protocol wrong type for socket", - 68:"Math result not representable", - 69:"Read only file system", - 70:"Illegal seek", - 71:"No such process", - 72:"Stale file handle", - 73:"Connection timed out", - 74:"Text file busy", - 75:"Cross-device link", - 100:"Device not a stream", - 101:"Bad font file fmt", - 102:"Invalid slot", - 103:"Invalid request code", - 104:"No anode", - 105:"Block device required", - 106:"Channel number out of range", - 107:"Level 3 halted", - 108:"Level 3 reset", - 109:"Link number out of range", - 110:"Protocol driver not attached", - 111:"No CSI structure available", - 112:"Level 2 halted", - 113:"Invalid exchange", - 114:"Invalid request descriptor", - 115:"Exchange full", - 116:"No data (for no delay io)", - 117:"Timer expired", - 118:"Out of streams resources", - 119:"Machine is not on the network", - 120:"Package not installed", - 121:"The object is remote", - 122:"Advertise error", - 123:"Srmount error", - 124:"Communication error on send", - 125:"Cross mount point (not really error)", - 126:"Given log. name not unique", - 127:"f.d. invalid for this operation", - 128:"Remote address changed", - 129:"Can access a needed shared lib", - 130:"Accessing a corrupted shared lib", - 131:".lib section in a.out corrupted", - 132:"Attempting to link in too many libs", - 133:"Attempting to exec a shared library", - 135:"Streams pipe error", - 136:"Too many users", - 137:"Socket type not supported", - 138:"Not supported", - 139:"Protocol family not supported", - 140:"Can't send after socket shutdown", - 141:"Too many references", - 142:"Host is down", - 148:"No medium (in tape drive)", - 156:"Level 2 not synchronized", }; - - - var demangle = (func) => { - warnOnce('warning: build with -sDEMANGLE_SUPPORT to link in libcxxabi demangling'); - return func; + + + + /** + * Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the + * emscripten HEAP, returns a copy of that string as a Javascript String object. + * + * @param {number} ptr + * @param {number=} maxBytesToRead - An optional length that specifies the + * maximum number of bytes to read. You can omit this parameter to scan the + * string until the first 0 byte. If maxBytesToRead is passed, and the string + * at [ptr, ptr+maxBytesToReadr[ contains a null byte in the middle, then the + * string will cut short at that byte index. + * @param {boolean=} ignoreNul - If true, the function will not stop on a NUL character. + * @return {string} + */ + var UTF8ToString = (ptr, maxBytesToRead, ignoreNul) => { + assert(typeof ptr == 'number', `UTF8ToString expects a number (got ${typeof ptr})`); + return ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead, ignoreNul) : ''; }; - var demangleAll = (text) => { - var regex = - /\b_Z[\w\d_]+/g; - return text.replace(regex, - function(x) { - var y = demangle(x); - return x === y ? x : (y + ' [' + x + ']'); - }); + + var strError = (errno) => UTF8ToString(_strerror(errno)); + + + var asyncLoad = async (url) => { + var arrayBuffer = await readAsync(url); + assert(arrayBuffer, `Loading data file "${url}" failed (no arrayBuffer).`); + return new Uint8Array(arrayBuffer); + }; + + + var FS_createDataFile = (...args) => FS.createDataFile(...args); + + var getUniqueRunDependency = (id) => { + var orig = id; + while (1) { + if (!runDependencyTracking[id]) return id; + id = orig + Math.random(); + } + }; + + + + var preloadPlugins = []; + var FS_handledByPreloadPlugin = async (byteArray, fullname) => { + // Ensure plugins are ready. + if (typeof Browser != 'undefined') Browser.init(); + + for (var plugin of preloadPlugins) { + if (plugin['canHandle'](fullname)) { + assert(plugin['handle'].constructor.name === 'AsyncFunction', 'Filesystem plugin handlers must be async functions (See #24914)') + return plugin['handle'](byteArray, fullname); + } + } + // If no plugin handled this file then return the original/unmodified + // byteArray. + return byteArray; + }; + var FS_preloadFile = async (parent, name, url, canRead, canWrite, dontCreateFile, canOwn, preFinish) => { + // TODO we should allow people to just pass in a complete filename instead + // of parent and name being that we just join them anyways + var fullname = name ? PATH_FS.resolve(PATH.join2(parent, name)) : parent; + var dep = getUniqueRunDependency(`cp ${fullname}`); // might have several active requests for the same fullname + addRunDependency(dep); + + try { + var byteArray = url; + if (typeof url == 'string') { + byteArray = await asyncLoad(url); + } + + byteArray = await FS_handledByPreloadPlugin(byteArray, fullname); + preFinish?.(); + if (!dontCreateFile) { + FS_createDataFile(parent, name, byteArray, canRead, canWrite, canOwn); + } + } finally { + removeRunDependency(dep); + } + }; + var FS_createPreloadedFile = (parent, name, url, canRead, canWrite, onload, onerror, dontCreateFile, canOwn, preFinish) => { + FS_preloadFile(parent, name, url, canRead, canWrite, dontCreateFile, canOwn, preFinish).then(onload).catch(onerror); }; var FS = { root:null, @@ -2738,69 +2411,173 @@ function dbg(text) { currentPath:"/", initialized:false, ignorePermissions:true, - ErrnoError:null, - genericErrors:{ - }, filesystems:null, syncFSRequests:0, - lookupPath(path, opts = {}) { - path = PATH_FS.resolve(path); - - if (!path) return { path: '', node: null }; - - var defaults = { - follow_mount: true, - recurse_count: 0 - }; - opts = Object.assign(defaults, opts) - - if (opts.recurse_count > 8) { // max recursive lookup of 8 - throw new FS.ErrnoError(32); + ErrnoError:class extends Error { + name = 'ErrnoError'; + // We set the `name` property to be able to identify `FS.ErrnoError` + // - the `name` is a standard ECMA-262 property of error objects. Kind of good to have it anyway. + // - when using PROXYFS, an error can come from an underlying FS + // as different FS objects have their own FS.ErrnoError each, + // the test `err instanceof FS.ErrnoError` won't detect an error coming from another filesystem, causing bugs. + // we'll use the reliable test `err.name == "ErrnoError"` instead + constructor(errno) { + super(runtimeInitialized ? strError(errno) : ''); + this.errno = errno; + for (var key in ERRNO_CODES) { + if (ERRNO_CODES[key] === errno) { + this.code = key; + break; + } + } } - - // split the absolute path - var parts = path.split('/').filter((p) => !!p); - - // start at the root - var current = FS.root; - var current_path = '/'; - - for (var i = 0; i < parts.length; i++) { - var islast = (i === parts.length-1); - if (islast && opts.parent) { - // stop resolving - break; + }, + FSStream:class { + shared = {}; + get object() { + return this.node; + } + set object(val) { + this.node = val; + } + get isRead() { + return (this.flags & 2097155) !== 1; + } + get isWrite() { + return (this.flags & 2097155) !== 0; + } + get isAppend() { + return (this.flags & 1024); + } + get flags() { + return this.shared.flags; + } + set flags(val) { + this.shared.flags = val; + } + get position() { + return this.shared.position; + } + set position(val) { + this.shared.position = val; + } + }, + FSNode:class { + node_ops = {}; + stream_ops = {}; + readMode = 292 | 73; + writeMode = 146; + mounted = null; + constructor(parent, name, mode, rdev) { + if (!parent) { + parent = this; // root node sets parent to itself } - - current = FS.lookupNode(current, parts[i]); - current_path = PATH.join2(current_path, parts[i]); - - // jump to the mount's root node if this is a mountpoint - if (FS.isMountpoint(current)) { - if (!islast || (islast && opts.follow_mount)) { + this.parent = parent; + this.mount = parent.mount; + this.id = FS.nextInode++; + this.name = name; + this.mode = mode; + this.rdev = rdev; + this.atime = this.mtime = this.ctime = Date.now(); + } + get read() { + return (this.mode & this.readMode) === this.readMode; + } + set read(val) { + val ? this.mode |= this.readMode : this.mode &= ~this.readMode; + } + get write() { + return (this.mode & this.writeMode) === this.writeMode; + } + set write(val) { + val ? this.mode |= this.writeMode : this.mode &= ~this.writeMode; + } + get isFolder() { + return FS.isDir(this.mode); + } + get isDevice() { + return FS.isChrdev(this.mode); + } + }, + lookupPath(path, opts = {}) { + if (!path) { + throw new FS.ErrnoError(44); + } + opts.follow_mount ??= true + + if (!PATH.isAbs(path)) { + path = FS.cwd() + '/' + path; + } + + // limit max consecutive symlinks to SYMLOOP_MAX. + linkloop: for (var nlinks = 0; nlinks < 40; nlinks++) { + // split the absolute path + var parts = path.split('/').filter((p) => !!p); + + // start at the root + var current = FS.root; + var current_path = '/'; + + for (var i = 0; i < parts.length; i++) { + var islast = (i === parts.length-1); + if (islast && opts.parent) { + // stop resolving + break; + } + + if (parts[i] === '.') { + continue; + } + + if (parts[i] === '..') { + current_path = PATH.dirname(current_path); + if (FS.isRoot(current)) { + path = current_path + '/' + parts.slice(i + 1).join('/'); + // We're making progress here, don't let many consecutive ..'s + // lead to ELOOP + nlinks--; + continue linkloop; + } else { + current = current.parent; + } + continue; + } + + current_path = PATH.join2(current_path, parts[i]); + try { + current = FS.lookupNode(current, parts[i]); + } catch (e) { + // if noent_okay is true, suppress a ENOENT in the last component + // and return an object with an undefined node. This is needed for + // resolving symlinks in the path when creating a file. + if ((e?.errno === 44) && islast && opts.noent_okay) { + return { path: current_path }; + } + throw e; + } + + // jump to the mount's root node if this is a mountpoint + if (FS.isMountpoint(current) && (!islast || opts.follow_mount)) { current = current.mounted.root; } - } - - // by default, lookupPath will not follow a symlink if it is the final path component. - // setting opts.follow = true will override this behavior. - if (!islast || opts.follow) { - var count = 0; - while (FS.isLink(current.mode)) { - var link = FS.readlink(current_path); - current_path = PATH_FS.resolve(PATH.dirname(current_path), link); - - var lookup = FS.lookupPath(current_path, { recurse_count: opts.recurse_count + 1 }); - current = lookup.node; - - if (count++ > 40) { // limit max consecutive symlinks to 40 (SYMLOOP_MAX). - throw new FS.ErrnoError(32); + + // by default, lookupPath will not follow a symlink if it is the final path component. + // setting opts.follow = true will override this behavior. + if (FS.isLink(current.mode) && (!islast || opts.follow)) { + if (!current.node_ops.readlink) { + throw new FS.ErrnoError(52); } + var link = current.node_ops.readlink(current); + if (!PATH.isAbs(link)) { + link = PATH.dirname(current_path) + '/' + link; + } + path = link + '/' + parts.slice(i + 1).join('/'); + continue linkloop; } } + return { path: current_path, node: current }; } - - return { path: current_path, node: current }; + throw new FS.ErrnoError(32); }, getPath(node) { var path; @@ -2816,7 +2593,7 @@ function dbg(text) { }, hashName(parentid, name) { var hash = 0; - + for (var i = 0; i < name.length; i++) { hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0; } @@ -2845,7 +2622,7 @@ function dbg(text) { lookupNode(parent, name) { var errCode = FS.mayLookup(parent); if (errCode) { - throw new FS.ErrnoError(errCode, parent); + throw new FS.ErrnoError(errCode); } var hash = FS.hashName(parent.id, name); for (var node = FS.nameTable[hash]; node; node = node.name_next) { @@ -2860,9 +2637,9 @@ function dbg(text) { createNode(parent, name, mode, rdev) { assert(typeof parent == 'object') var node = new FS.FSNode(parent, name, mode, rdev); - + FS.hashAddNode(node); - + return node; }, destroyNode(node) { @@ -2909,20 +2686,26 @@ function dbg(text) { // return 0 if any user, group or owner bits are set. if (perms.includes('r') && !(node.mode & 292)) { return 2; - } else if (perms.includes('w') && !(node.mode & 146)) { + } + if (perms.includes('w') && !(node.mode & 146)) { return 2; - } else if (perms.includes('x') && !(node.mode & 73)) { + } + if (perms.includes('x') && !(node.mode & 73)) { return 2; } return 0; }, mayLookup(dir) { + if (!FS.isDir(dir.mode)) return 54; var errCode = FS.nodePermissions(dir, 'x'); if (errCode) return errCode; if (!dir.node_ops.lookup) return 2; return 0; }, mayCreate(dir, name) { + if (!FS.isDir(dir.mode)) { + return 54; + } try { var node = FS.lookupNode(dir, name); return 20; @@ -2948,10 +2731,8 @@ function dbg(text) { if (FS.isRoot(node) || FS.getPath(node) === FS.cwd()) { return 10; } - } else { - if (FS.isDir(node.mode)) { - return 31; - } + } else if (FS.isDir(node.mode)) { + return 31; } return 0; }, @@ -2961,13 +2742,22 @@ function dbg(text) { } if (FS.isLink(node.mode)) { return 32; - } else if (FS.isDir(node.mode)) { - if (FS.flagsToPermissionString(flags) !== 'r' || // opening for write - (flags & 512)) { // TODO: check for O_SEARCH? (== search for dir only) + } + var mode = FS.flagsToPermissionString(flags); + if (FS.isDir(node.mode)) { + // opening for write + // TODO: check for O_SEARCH? (== search for dir only) + if (mode !== 'r' || (flags & (512 | 64))) { return 31; } } - return FS.nodePermissions(node, FS.flagsToPermissionString(flags)); + return FS.nodePermissions(node, mode); + }, + checkOpExists(op, err) { + if (!op) { + throw new FS.ErrnoError(err); + } + return op; }, MAX_OPEN_FDS:4096, nextfd() { @@ -2978,53 +2768,17 @@ function dbg(text) { } throw new FS.ErrnoError(33); }, - getStreamChecked(fd) { - var stream = FS.getStream(fd); - if (!stream) { - throw new FS.ErrnoError(8); - } - return stream; - }, - getStream:(fd) => FS.streams[fd], - createStream(stream, fd = -1) { - if (!FS.FSStream) { - FS.FSStream = /** @constructor */ function() { - this.shared = { }; - }; - FS.FSStream.prototype = {}; - Object.defineProperties(FS.FSStream.prototype, { - object: { - /** @this {FS.FSStream} */ - get() { return this.node; }, - /** @this {FS.FSStream} */ - set(val) { this.node = val; } - }, - isRead: { - /** @this {FS.FSStream} */ - get() { return (this.flags & 2097155) !== 1; } - }, - isWrite: { - /** @this {FS.FSStream} */ - get() { return (this.flags & 2097155) !== 0; } - }, - isAppend: { - /** @this {FS.FSStream} */ - get() { return (this.flags & 1024); } - }, - flags: { - /** @this {FS.FSStream} */ - get() { return this.shared.flags; }, - /** @this {FS.FSStream} */ - set(val) { this.shared.flags = val; }, - }, - position : { - /** @this {FS.FSStream} */ - get() { return this.shared.position; }, - /** @this {FS.FSStream} */ - set(val) { this.shared.position = val; }, - }, - }); + getStreamChecked(fd) { + var stream = FS.getStream(fd); + if (!stream) { + throw new FS.ErrnoError(8); } + return stream; + }, + getStream:(fd) => FS.streams[fd], + createStream(stream, fd = -1) { + assert(fd >= -1); + // clone it, so we can return an instance of FSStream stream = Object.assign(new FS.FSStream(), stream); if (fd == -1) { @@ -3037,15 +2791,32 @@ function dbg(text) { closeStream(fd) { FS.streams[fd] = null; }, + dupStream(origStream, fd = -1) { + var stream = FS.createStream(origStream, fd); + stream.stream_ops?.dup?.(stream); + return stream; + }, + doSetAttr(stream, node, attr) { + var setattr = stream?.stream_ops.setattr; + var arg = setattr ? stream : node; + setattr ??= node.node_ops.setattr; + FS.checkOpExists(setattr, 63) + try { + setattr(arg, attr); + } catch (e) { + if (e instanceof RangeError) { + throw new FS.ErrnoError(22); + } + throw e; + } + }, chrdev_stream_ops:{ open(stream) { var device = FS.getDevice(stream.node.rdev); // override node's stream ops with the device's stream.stream_ops = device.stream_ops; // forward the open call - if (stream.stream_ops.open) { - stream.stream_ops.open(stream); - } + stream.stream_ops.open?.(stream); }, llseek() { throw new FS.ErrnoError(70); @@ -3061,15 +2832,15 @@ function dbg(text) { getMounts(mount) { var mounts = []; var check = [mount]; - + while (check.length) { var m = check.pop(); - + mounts.push(m); - - check.push.apply(check, m.mounts); + + check.push(...m.mounts); } - + return mounts; }, syncfs(populate, callback) { @@ -3077,22 +2848,22 @@ function dbg(text) { callback = populate; populate = false; } - + FS.syncFSRequests++; - + if (FS.syncFSRequests > 1) { err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`); } - + var mounts = FS.getMounts(FS.root.mount); var completed = 0; - + function doCallback(errCode) { assert(FS.syncFSRequests > 0); FS.syncFSRequests--; return callback(errCode); } - + function done(errCode) { if (errCode) { if (!done.errored) { @@ -3105,14 +2876,15 @@ function dbg(text) { doCallback(null); } }; - + // sync all mounts - mounts.forEach((mount) => { - if (!mount.type.syncfs) { - return done(null); + for (var mount of mounts) { + if (mount.type.syncfs) { + mount.type.syncfs(mount, populate, done); + } else { + done(null); } - mount.type.syncfs(mount, populate, done); - }); + } }, mount(type, opts, mountpoint) { if (typeof type == 'string') { @@ -3123,79 +2895,77 @@ function dbg(text) { var root = mountpoint === '/'; var pseudo = !mountpoint; var node; - + if (root && FS.root) { throw new FS.ErrnoError(10); } else if (!root && !pseudo) { var lookup = FS.lookupPath(mountpoint, { follow_mount: false }); - + mountpoint = lookup.path; // use the absolute path node = lookup.node; - + if (FS.isMountpoint(node)) { throw new FS.ErrnoError(10); } - + if (!FS.isDir(node.mode)) { throw new FS.ErrnoError(54); } } - + var mount = { type, opts, mountpoint, mounts: [] }; - + // create a root node for the fs var mountRoot = type.mount(mount); mountRoot.mount = mount; mount.root = mountRoot; - + if (root) { FS.root = mountRoot; } else if (node) { // set as a mountpoint node.mounted = mount; - + // add the new mount to the current mount's children if (node.mount) { node.mount.mounts.push(mount); } } - + return mountRoot; }, unmount(mountpoint) { var lookup = FS.lookupPath(mountpoint, { follow_mount: false }); - + if (!FS.isMountpoint(lookup.node)) { throw new FS.ErrnoError(28); } - + // destroy the nodes for this mount, and all its child mounts var node = lookup.node; var mount = node.mounted; var mounts = FS.getMounts(mount); - - Object.keys(FS.nameTable).forEach((hash) => { - var current = FS.nameTable[hash]; - + + for (var [hash, current] of Object.entries(FS.nameTable)) { while (current) { var next = current.name_next; - + if (mounts.includes(current.mount)) { FS.destroyNode(current); } - + current = next; } - }); - + } + // no longer a mountpoint node.mounted = null; - + // remove this mount from the child mounts var idx = node.mount.mounts.indexOf(mount); assert(idx !== -1); @@ -3208,9 +2978,12 @@ function dbg(text) { var lookup = FS.lookupPath(path, { parent: true }); var parent = lookup.node; var name = PATH.basename(path); - if (!name || name === '.' || name === '..') { + if (!name) { throw new FS.ErrnoError(28); } + if (name === '.' || name === '..') { + throw new FS.ErrnoError(20); + } var errCode = FS.mayCreate(parent, name); if (errCode) { throw new FS.ErrnoError(errCode); @@ -3220,14 +2993,43 @@ function dbg(text) { } return parent.node_ops.mknod(parent, name, mode, dev); }, - create(path, mode) { - mode = mode !== undefined ? mode : 438 /* 0666 */; + statfs(path) { + return FS.statfsNode(FS.lookupPath(path, {follow: true}).node); + }, + statfsStream(stream) { + // We keep a separate statfsStream function because noderawfs overrides + // it. In noderawfs, stream.node is sometimes null. Instead, we need to + // look at stream.path. + return FS.statfsNode(stream.node); + }, + statfsNode(node) { + // NOTE: None of the defaults here are true. We're just returning safe and + // sane values. Currently nodefs and rawfs replace these defaults, + // other file systems leave them alone. + var rtn = { + bsize: 4096, + frsize: 4096, + blocks: 1e6, + bfree: 5e5, + bavail: 5e5, + files: FS.nextInode, + ffree: FS.nextInode - 1, + fsid: 42, + flags: 2, + namelen: 255, + }; + + if (node.node_ops.statfs) { + Object.assign(rtn, node.node_ops.statfs(node.mount.opts.root)); + } + return rtn; + }, + create(path, mode = 0o666) { mode &= 4095; mode |= 32768; return FS.mknod(path, mode, 0); }, - mkdir(path, mode) { - mode = mode !== undefined ? mode : 511 /* 0777 */; + mkdir(path, mode = 0o777) { mode &= 511 | 512; mode |= 16384; return FS.mknod(path, mode, 0); @@ -3235,9 +3037,10 @@ function dbg(text) { mkdirTree(path, mode) { var dirs = path.split('/'); var d = ''; - for (var i = 0; i < dirs.length; ++i) { - if (!dirs[i]) continue; - d += '/' + dirs[i]; + for (var dir of dirs) { + if (!dir) continue; + if (d || PATH.isAbs(path)) d += '/'; + d += dir; try { FS.mkdir(d, mode); } catch(e) { @@ -3248,7 +3051,7 @@ function dbg(text) { mkdev(path, mode, dev) { if (typeof dev == 'undefined') { dev = mode; - mode = 438 /* 0666 */; + mode = 0o666; } mode |= 8192; return FS.mknod(path, mode, dev); @@ -3279,13 +3082,13 @@ function dbg(text) { var new_name = PATH.basename(new_path); // parents must exist var lookup, old_dir, new_dir; - - // let the errors from non existant directories percolate up + + // let the errors from non existent directories percolate up lookup = FS.lookupPath(old_path, { parent: true }); old_dir = lookup.node; lookup = FS.lookupPath(new_path, { parent: true }); new_dir = lookup.node; - + if (!old_dir || !new_dir) throw new FS.ErrnoError(44); // need to be part of the same mount if (old_dir.mount !== new_dir.mount) { @@ -3346,6 +3149,9 @@ function dbg(text) { // do the underlying fs rename try { old_dir.node_ops.rename(old_node, new_dir, new_name); + // update old node (we do this here to avoid each backend + // needing to) + old_node.parent = new_dir; } catch (e) { throw e; } finally { @@ -3375,10 +3181,8 @@ function dbg(text) { readdir(path) { var lookup = FS.lookupPath(path, { follow: true }); var node = lookup.node; - if (!node.node_ops.readdir) { - throw new FS.ErrnoError(54); - } - return node.node_ops.readdir(node); + var readdir = FS.checkOpExists(node.node_ops.readdir, 54); + return readdir(node); }, unlink(path) { var lookup = FS.lookupPath(path, { parent: true }); @@ -3413,22 +3217,33 @@ function dbg(text) { if (!link.node_ops.readlink) { throw new FS.ErrnoError(28); } - return PATH_FS.resolve(FS.getPath(link.parent), link.node_ops.readlink(link)); + return link.node_ops.readlink(link); }, stat(path, dontFollow) { var lookup = FS.lookupPath(path, { follow: !dontFollow }); var node = lookup.node; - if (!node) { - throw new FS.ErrnoError(44); - } - if (!node.node_ops.getattr) { - throw new FS.ErrnoError(63); - } - return node.node_ops.getattr(node); + var getattr = FS.checkOpExists(node.node_ops.getattr, 63); + return getattr(node); + }, + fstat(fd) { + var stream = FS.getStreamChecked(fd); + var node = stream.node; + var getattr = stream.stream_ops.getattr; + var arg = getattr ? stream : node; + getattr ??= node.node_ops.getattr; + FS.checkOpExists(getattr, 63) + return getattr(arg); }, lstat(path) { return FS.stat(path, true); }, + doChmod(stream, node, mode, dontFollow) { + FS.doSetAttr(stream, node, { + mode: (mode & 4095) | (node.mode & ~4095), + ctime: Date.now(), + dontFollow + }); + }, chmod(path, mode, dontFollow) { var node; if (typeof path == 'string') { @@ -3437,20 +3252,21 @@ function dbg(text) { } else { node = path; } - if (!node.node_ops.setattr) { - throw new FS.ErrnoError(63); - } - node.node_ops.setattr(node, { - mode: (mode & 4095) | (node.mode & ~4095), - timestamp: Date.now() - }); + FS.doChmod(null, node, mode, dontFollow); }, lchmod(path, mode) { FS.chmod(path, mode, true); }, fchmod(fd, mode) { var stream = FS.getStreamChecked(fd); - FS.chmod(stream.node, mode); + FS.doChmod(stream, stream.node, mode, false); + }, + doChown(stream, node, dontFollow) { + FS.doSetAttr(stream, node, { + timestamp: Date.now(), + dontFollow + // we ignore the uid / gid for now + }); }, chown(path, uid, gid, dontFollow) { var node; @@ -3460,35 +3276,16 @@ function dbg(text) { } else { node = path; } - if (!node.node_ops.setattr) { - throw new FS.ErrnoError(63); - } - node.node_ops.setattr(node, { - timestamp: Date.now() - // we ignore the uid / gid for now - }); + FS.doChown(null, node, dontFollow); }, lchown(path, uid, gid) { FS.chown(path, uid, gid, true); }, fchown(fd, uid, gid) { var stream = FS.getStreamChecked(fd); - FS.chown(stream.node, uid, gid); + FS.doChown(stream, stream.node, false); }, - truncate(path, len) { - if (len < 0) { - throw new FS.ErrnoError(28); - } - var node; - if (typeof path == 'string') { - var lookup = FS.lookupPath(path, { follow: true }); - node = lookup.node; - } else { - node = path; - } - if (!node.node_ops.setattr) { - throw new FS.ErrnoError(63); - } + doTruncate(stream, node, len) { if (FS.isDir(node.mode)) { throw new FS.ErrnoError(31); } @@ -3499,49 +3296,65 @@ function dbg(text) { if (errCode) { throw new FS.ErrnoError(errCode); } - node.node_ops.setattr(node, { + FS.doSetAttr(stream, node, { size: len, timestamp: Date.now() }); }, + truncate(path, len) { + if (len < 0) { + throw new FS.ErrnoError(28); + } + var node; + if (typeof path == 'string') { + var lookup = FS.lookupPath(path, { follow: true }); + node = lookup.node; + } else { + node = path; + } + FS.doTruncate(null, node, len); + }, ftruncate(fd, len) { var stream = FS.getStreamChecked(fd); - if ((stream.flags & 2097155) === 0) { + if (len < 0 || (stream.flags & 2097155) === 0) { throw new FS.ErrnoError(28); } - FS.truncate(stream.node, len); + FS.doTruncate(stream, stream.node, len); }, utime(path, atime, mtime) { var lookup = FS.lookupPath(path, { follow: true }); var node = lookup.node; - node.node_ops.setattr(node, { - timestamp: Math.max(atime, mtime) + var setattr = FS.checkOpExists(node.node_ops.setattr, 63); + setattr(node, { + atime: atime, + mtime: mtime }); }, - open(path, flags, mode) { + open(path, flags, mode = 0o666) { if (path === "") { throw new FS.ErrnoError(44); } - flags = typeof flags == 'string' ? FS_modeStringToFlags(flags) : flags; - mode = typeof mode == 'undefined' ? 438 /* 0666 */ : mode; + flags = FS_modeStringToFlags(flags); if ((flags & 64)) { mode = (mode & 4095) | 32768; } else { mode = 0; } var node; + var isDirPath; if (typeof path == 'object') { node = path; } else { - path = PATH.normalize(path); - try { - var lookup = FS.lookupPath(path, { - follow: !(flags & 131072) - }); - node = lookup.node; - } catch (e) { - // ignore - } + isDirPath = path.endsWith("/"); + // noent_okay makes it so that if the final component of the path + // doesn't exist, lookupPath returns `node: undefined`. `path` will be + // updated to point to the target of all symlinks. + var lookup = FS.lookupPath(path, { + follow: !(flags & 131072), + noent_okay: true + }); + node = lookup.node; + path = lookup.path; } // perhaps we need to create the node var created = false; @@ -3551,9 +3364,14 @@ function dbg(text) { if ((flags & 128)) { throw new FS.ErrnoError(20); } + } else if (isDirPath) { + throw new FS.ErrnoError(31); } else { // node doesn't exist, try to create it - node = FS.mknod(path, mode, 0); + // Ignore the permission bits here to ensure we can `open` this new + // file below. We use chmod below to apply the permissions once the + // file is open. + node = FS.mknod(path, mode | 0o777, 0); created = true; } } @@ -3583,7 +3401,7 @@ function dbg(text) { } // we've already handled these, don't pass down to the underlying vfs flags &= ~(128 | 512 | 131072); - + // register the stream with the filesystem var stream = FS.createStream({ node, @@ -3600,11 +3418,8 @@ function dbg(text) { if (stream.stream_ops.open) { stream.stream_ops.open(stream); } - if (Module['logReadFiles'] && !(flags & 1)) { - if (!FS.readFiles) FS.readFiles = {}; - if (!(path in FS.readFiles)) { - FS.readFiles[path] = 1; - } + if (created) { + FS.chmod(node, mode & 0o777); } return stream; }, @@ -3670,6 +3485,7 @@ function dbg(text) { }, write(stream, buffer, offset, length, position, canOwn) { assert(offset >= 0); + assert(buffer.subarray, 'FS.write expects a TypedArray'); if (length < 0 || position < 0) { throw new FS.ErrnoError(28); } @@ -3699,24 +3515,6 @@ function dbg(text) { if (!seeking) stream.position += bytesWritten; return bytesWritten; }, - allocate(stream, offset, length) { - if (FS.isClosed(stream)) { - throw new FS.ErrnoError(8); - } - if (offset < 0 || length <= 0) { - throw new FS.ErrnoError(28); - } - if ((stream.flags & 2097155) === 0) { - throw new FS.ErrnoError(8); - } - if (!FS.isFile(stream.node.mode) && !FS.isDir(stream.node.mode)) { - throw new FS.ErrnoError(43); - } - if (!stream.stream_ops.allocate) { - throw new FS.ErrnoError(138); - } - stream.stream_ops.allocate(stream, offset, length); - }, mmap(stream, length, position, prot, flags) { // User requests writing to file (prot & PROT_WRITE != 0). // Checking if we have permissions to write to the file unless @@ -3735,6 +3533,9 @@ function dbg(text) { if (!stream.stream_ops.mmap) { throw new FS.ErrnoError(43); } + if (!length) { + throw new FS.ErrnoError(28); + } return stream.stream_ops.mmap(stream, length, position, prot, flags); }, msync(stream, buffer, offset, length, mmapFlags) { @@ -3744,7 +3545,6 @@ function dbg(text) { } return stream.stream_ops.msync(stream, buffer, offset, length, mmapFlags); }, - munmap:(stream) => 0, ioctl(stream, cmd, arg) { if (!stream.stream_ops.ioctl) { throw new FS.ErrnoError(59); @@ -3755,34 +3555,24 @@ function dbg(text) { opts.flags = opts.flags || 0; opts.encoding = opts.encoding || 'binary'; if (opts.encoding !== 'utf8' && opts.encoding !== 'binary') { - throw new Error(`Invalid encoding type "${opts.encoding}"`); + abort(`Invalid encoding type "${opts.encoding}"`); } - var ret; var stream = FS.open(path, opts.flags); var stat = FS.stat(path); var length = stat.size; var buf = new Uint8Array(length); FS.read(stream, buf, 0, length, 0); if (opts.encoding === 'utf8') { - ret = UTF8ArrayToString(buf, 0); - } else if (opts.encoding === 'binary') { - ret = buf; + buf = UTF8ArrayToString(buf); } FS.close(stream); - return ret; + return buf; }, writeFile(path, data, opts = {}) { opts.flags = opts.flags || 577; var stream = FS.open(path, opts.flags, opts.mode); - if (typeof data == 'string') { - var buf = new Uint8Array(lengthBytesUTF8(data)+1); - var actualNumBytes = stringToUTF8Array(data, buf, 0, buf.length); - FS.write(stream, buf, 0, actualNumBytes, undefined, opts.canOwn); - } else if (ArrayBuffer.isView(data)) { - FS.write(stream, data, 0, data.byteLength, undefined, opts.canOwn); - } else { - throw new Error('Unsupported data type'); - } + data = FS_fileDataToTypedArray(data); + FS.write(stream, data, 0, data.byteLength, undefined, opts.canOwn); FS.close(stream); }, cwd:() => FS.currentPath, @@ -3812,6 +3602,7 @@ function dbg(text) { FS.registerDevice(FS.makedev(1, 3), { read: () => 0, write: (stream, buffer, offset, length, pos) => length, + llseek: () => 0, }); FS.mkdev('/dev/null', FS.makedev(1, 3)); // setup /dev/tty and /dev/tty1 @@ -3826,7 +3617,8 @@ function dbg(text) { var randomBuffer = new Uint8Array(1024), randomLeft = 0; var randomByte = () => { if (randomLeft === 0) { - randomLeft = randomFill(randomBuffer).byteLength; + randomFill(randomBuffer); + randomLeft = randomBuffer.byteLength; } return randomBuffer[--randomLeft]; }; @@ -3845,7 +3637,10 @@ function dbg(text) { FS.mkdir('/proc/self/fd'); FS.mount({ mount() { - var node = FS.createNode(proc_self, 'fd', 16384 | 511 /* 0777 */, 73); + var node = FS.createNode(proc_self, 'fd', 16895, 73); + node.stream_ops = { + llseek: MEMFS.stream_ops.llseek, + }; node.node_ops = { lookup(parent, name) { var fd = +name; @@ -3854,40 +3649,46 @@ function dbg(text) { parent: null, mount: { mountpoint: 'fake' }, node_ops: { readlink: () => stream.path }, + id: fd + 1, }; ret.parent = ret; // make it look like a simple root node return ret; + }, + readdir() { + return Array.from(FS.streams.entries()) + .filter(([k, v]) => v) + .map(([k, v]) => k.toString()); } }; return node; } }, {}, '/proc/self/fd'); }, - createStandardStreams() { + createStandardStreams(input, output, error) { // TODO deprecate the old functionality of a single // input / output callback and that utilizes FS.createDevice // and instead require a unique set of stream ops - + // by default, we symlink the standard streams to the // default tty devices. however, if the standard streams // have been overwritten we create a unique device for // them instead. - if (Module['stdin']) { - FS.createDevice('/dev', 'stdin', Module['stdin']); + if (input) { + FS.createDevice('/dev', 'stdin', input); } else { FS.symlink('/dev/tty', '/dev/stdin'); } - if (Module['stdout']) { - FS.createDevice('/dev', 'stdout', null, Module['stdout']); + if (output) { + FS.createDevice('/dev', 'stdout', null, output); } else { FS.symlink('/dev/tty', '/dev/stdout'); } - if (Module['stderr']) { - FS.createDevice('/dev', 'stderr', null, Module['stderr']); + if (error) { + FS.createDevice('/dev', 'stderr', null, error); } else { FS.symlink('/dev/tty1', '/dev/stderr'); } - + // open default streams for the stdin, stdout and stderr devices var stdin = FS.open('/dev/stdin', 0); var stdout = FS.open('/dev/stdout', 1); @@ -3896,85 +3697,40 @@ function dbg(text) { assert(stdout.fd === 1, `invalid handle for stdout (${stdout.fd})`); assert(stderr.fd === 2, `invalid handle for stderr (${stderr.fd})`); }, - ensureErrnoError() { - if (FS.ErrnoError) return; - FS.ErrnoError = /** @this{Object} */ function ErrnoError(errno, node) { - // We set the `name` property to be able to identify `FS.ErrnoError` - // - the `name` is a standard ECMA-262 property of error objects. Kind of good to have it anyway. - // - when using PROXYFS, an error can come from an underlying FS - // as different FS objects have their own FS.ErrnoError each, - // the test `err instanceof FS.ErrnoError` won't detect an error coming from another filesystem, causing bugs. - // we'll use the reliable test `err.name == "ErrnoError"` instead - this.name = 'ErrnoError'; - this.node = node; - this.setErrno = /** @this{Object} */ function(errno) { - this.errno = errno; - for (var key in ERRNO_CODES) { - if (ERRNO_CODES[key] === errno) { - this.code = key; - break; - } - } - }; - this.setErrno(errno); - this.message = ERRNO_MESSAGES[errno]; - - // Try to get a maximally helpful stack trace. On Node.js, getting Error.stack - // now ensures it shows what we want. - if (this.stack) { - // Define the stack property for Node.js 4, which otherwise errors on the next line. - Object.defineProperty(this, "stack", { value: (new Error).stack, writable: true }); - this.stack = demangleAll(this.stack); - } - }; - FS.ErrnoError.prototype = new Error(); - FS.ErrnoError.prototype.constructor = FS.ErrnoError; - // Some errors may happen quite a bit, to avoid overhead we reuse them (and suffer a lack of stack info) - [44].forEach((code) => { - FS.genericErrors[code] = new FS.ErrnoError(code); - FS.genericErrors[code].stack = ''; - }); - }, staticInit() { - FS.ensureErrnoError(); - FS.nameTable = new Array(4096); - + FS.mount(MEMFS, {}, '/'); - + FS.createDefaultDirectories(); FS.createDefaultDevices(); FS.createSpecialDirectories(); - + FS.filesystems = { 'MEMFS': MEMFS, 'NODEFS': NODEFS, }; }, init(input, output, error) { - assert(!FS.init.initialized, 'FS.init was previously called. If you want to initialize later with custom parameters, remove any earlier calls (note that one is automatically added to the generated code)'); - FS.init.initialized = true; - - FS.ensureErrnoError(); - + assert(!FS.initialized, 'FS.init was previously called. If you want to initialize later with custom parameters, remove any earlier calls (note that one is automatically added to the generated code)'); + FS.initialized = true; + // Allow Module.stdin etc. to provide defaults, if none explicitly passed to us here - Module['stdin'] = input || Module['stdin']; - Module['stdout'] = output || Module['stdout']; - Module['stderr'] = error || Module['stderr']; - - FS.createStandardStreams(); + input ??= Module['stdin']; + output ??= Module['stdout']; + error ??= Module['stderr']; + + FS.createStandardStreams(input, output, error); }, quit() { - FS.init.initialized = false; + FS.initialized = false; // force-flush all streams, so we get musl std streams printed out _fflush(0); // close all of our streams - for (var i = 0; i < FS.streams.length; i++) { - var stream = FS.streams[i]; - if (!stream) { - continue; + for (var stream of FS.streams) { + if (stream) { + FS.close(stream); } - FS.close(stream); } }, findObject(path, dontResolveLastLink) { @@ -4022,7 +3778,7 @@ function dbg(text) { try { FS.mkdir(current); } catch (e) { - // ignore EEXIST + if (e.errno != 20) throw e; } parent = current; } @@ -4042,11 +3798,7 @@ function dbg(text) { var mode = FS_getMode(canRead, canWrite); var node = FS.create(path, mode); if (data) { - if (typeof data == 'string') { - var arr = new Array(data.length); - for (var i = 0, len = data.length; i < len; ++i) arr[i] = data.charCodeAt(i); - data = arr; - } + data = FS_fileDataToTypedArray(data); // make sure we can write to the file FS.chmod(node, mode | 146); var stream = FS.open(node, 577); @@ -4058,7 +3810,7 @@ function dbg(text) { createDevice(parent, name, input, output) { var path = PATH.join2(typeof parent == 'string' ? parent : FS.getPath(parent), name); var mode = FS_getMode(!!input, !!output); - if (!FS.createDevice.major) FS.createDevice.major = 64; + FS.createDevice.major ??= 64; var dev = FS.makedev(FS.createDevice.major++, 0); // Create a fake device that a set of stream ops to emulate // the old behavior. @@ -4068,7 +3820,7 @@ function dbg(text) { }, close(stream) { // flush any pending line data - if (output && output.buffer && output.buffer.length) { + if (output?.buffer?.length) { output(10); } }, @@ -4089,7 +3841,7 @@ function dbg(text) { buffer[offset+i] = result; } if (bytesRead) { - stream.node.timestamp = Date.now(); + stream.node.atime = Date.now(); } return bytesRead; }, @@ -4102,7 +3854,7 @@ function dbg(text) { } } if (length) { - stream.node.timestamp = Date.now(); + stream.node.mtime = stream.node.ctime = Date.now(); } return i; } @@ -4111,129 +3863,117 @@ function dbg(text) { }, forceLoadFile(obj) { if (obj.isDevice || obj.isFolder || obj.link || obj.contents) return true; - if (typeof XMLHttpRequest != 'undefined') { - throw new Error("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread."); - } else if (read_) { - // Command-line. + if (globalThis.XMLHttpRequest) { + abort("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread."); + } else { // Command-line. try { - // WARNING: Can't read binary files in V8's d8 or tracemonkey's js, as - // read() will try to parse UTF8. - obj.contents = intArrayFromString(read_(obj.url), true); - obj.usedBytes = obj.contents.length; + obj.contents = readBinary(obj.url); } catch (e) { throw new FS.ErrnoError(29); } - } else { - throw new Error('Cannot load without read() or XMLHttpRequest.'); } }, createLazyFile(parent, name, url, canRead, canWrite) { - // Lazy chunked Uint8Array (implements get and length from Uint8Array). Actual getting is abstracted away for eventual reuse. - /** @constructor */ - function LazyUint8Array() { - this.lengthKnown = false; - this.chunks = []; // Loaded chunks. Index is the chunk number - } - LazyUint8Array.prototype.get = /** @this{Object} */ function LazyUint8Array_get(idx) { - if (idx > this.length-1 || idx < 0) { - return undefined; + // Lazy chunked Uint8Array (implements get and length from Uint8Array). + // Actual getting is abstracted away for eventual reuse. + class LazyUint8Array { + lengthKnown = false; + chunks = []; // Loaded chunks. Index is the chunk number + get(idx) { + if (idx > this.length-1 || idx < 0) { + return undefined; + } + var chunkOffset = idx % this.chunkSize; + var chunkNum = (idx / this.chunkSize)|0; + return this.getter(chunkNum)[chunkOffset]; } - var chunkOffset = idx % this.chunkSize; - var chunkNum = (idx / this.chunkSize)|0; - return this.getter(chunkNum)[chunkOffset]; - }; - LazyUint8Array.prototype.setDataGetter = function LazyUint8Array_setDataGetter(getter) { - this.getter = getter; - }; - LazyUint8Array.prototype.cacheLength = function LazyUint8Array_cacheLength() { - // Find length - var xhr = new XMLHttpRequest(); - xhr.open('HEAD', url, false); - xhr.send(null); - if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) throw new Error("Couldn't load " + url + ". Status: " + xhr.status); - var datalength = Number(xhr.getResponseHeader("Content-length")); - var header; - var hasByteServing = (header = xhr.getResponseHeader("Accept-Ranges")) && header === "bytes"; - var usesGzip = (header = xhr.getResponseHeader("Content-Encoding")) && header === "gzip"; - - var chunkSize = 1024*1024; // Chunk size in bytes - - if (!hasByteServing) chunkSize = datalength; - - // Function to get a range from the remote URL. - var doXHR = (from, to) => { - if (from > to) throw new Error("invalid range (" + from + ", " + to + ") or no bytes requested!"); - if (to > datalength-1) throw new Error("only " + datalength + " bytes available! programmer error!"); - - // TODO: Use mozResponseArrayBuffer, responseStream, etc. if available. + setDataGetter(getter) { + this.getter = getter; + } + cacheLength() { + // Find length var xhr = new XMLHttpRequest(); - xhr.open('GET', url, false); - if (datalength !== chunkSize) xhr.setRequestHeader("Range", "bytes=" + from + "-" + to); - - // Some hints to the browser that we want binary data. - xhr.responseType = 'arraybuffer'; - if (xhr.overrideMimeType) { - xhr.overrideMimeType('text/plain; charset=x-user-defined'); - } - + xhr.open('HEAD', url, false); xhr.send(null); - if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) throw new Error("Couldn't load " + url + ". Status: " + xhr.status); - if (xhr.response !== undefined) { - return new Uint8Array(/** @type{Array} */(xhr.response || [])); + if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) abort("Couldn't load " + url + ". Status: " + xhr.status); + var datalength = Number(xhr.getResponseHeader("Content-length")); + var header; + var hasByteServing = (header = xhr.getResponseHeader("Accept-Ranges")) && header === "bytes"; + var usesGzip = (header = xhr.getResponseHeader("Content-Encoding")) && header === "gzip"; + + var chunkSize = 1024*1024; // Chunk size in bytes + + if (!hasByteServing) chunkSize = datalength; + + // Function to get a range from the remote URL. + var doXHR = (from, to) => { + if (from > to) abort("invalid range (" + from + ", " + to + ") or no bytes requested!"); + if (to > datalength-1) abort("only " + datalength + " bytes available! programmer error!"); + + // TODO: Use mozResponseArrayBuffer, responseStream, etc. if available. + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, false); + if (datalength !== chunkSize) xhr.setRequestHeader("Range", "bytes=" + from + "-" + to); + + // Some hints to the browser that we want binary data. + xhr.responseType = 'arraybuffer'; + if (xhr.overrideMimeType) { + xhr.overrideMimeType('text/plain; charset=x-user-defined'); + } + + xhr.send(null); + if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) abort("Couldn't load " + url + ". Status: " + xhr.status); + if (xhr.response !== undefined) { + return new Uint8Array(/** @type{Array} */(xhr.response || [])); + } + return intArrayFromString(xhr.responseText || '', true); + }; + var lazyArray = this; + lazyArray.setDataGetter((chunkNum) => { + var start = chunkNum * chunkSize; + var end = (chunkNum+1) * chunkSize - 1; // including this byte + end = Math.min(end, datalength-1); // if datalength-1 is selected, this is the last block + if (typeof lazyArray.chunks[chunkNum] == 'undefined') { + lazyArray.chunks[chunkNum] = doXHR(start, end); + } + if (typeof lazyArray.chunks[chunkNum] == 'undefined') abort('doXHR failed!'); + return lazyArray.chunks[chunkNum]; + }); + + if (usesGzip || !datalength) { + // if the server uses gzip or doesn't supply the length, we have to download the whole file to get the (uncompressed) length + chunkSize = datalength = 1; // this will force getter(0)/doXHR do download the whole file + datalength = this.getter(0).length; + chunkSize = datalength; + out("LazyFiles on gzip forces download of the whole file when length is accessed"); } - return intArrayFromString(xhr.responseText || '', true); - }; - var lazyArray = this; - lazyArray.setDataGetter((chunkNum) => { - var start = chunkNum * chunkSize; - var end = (chunkNum+1) * chunkSize - 1; // including this byte - end = Math.min(end, datalength-1); // if datalength-1 is selected, this is the last block - if (typeof lazyArray.chunks[chunkNum] == 'undefined') { - lazyArray.chunks[chunkNum] = doXHR(start, end); + + this._length = datalength; + this._chunkSize = chunkSize; + this.lengthKnown = true; + } + get length() { + if (!this.lengthKnown) { + this.cacheLength(); } - if (typeof lazyArray.chunks[chunkNum] == 'undefined') throw new Error('doXHR failed!'); - return lazyArray.chunks[chunkNum]; - }); - - if (usesGzip || !datalength) { - // if the server uses gzip or doesn't supply the length, we have to download the whole file to get the (uncompressed) length - chunkSize = datalength = 1; // this will force getter(0)/doXHR do download the whole file - datalength = this.getter(0).length; - chunkSize = datalength; - out("LazyFiles on gzip forces download of the whole file when length is accessed"); + return this._length; } - - this._length = datalength; - this._chunkSize = chunkSize; - this.lengthKnown = true; - }; - if (typeof XMLHttpRequest != 'undefined') { - if (!ENVIRONMENT_IS_WORKER) throw 'Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc'; - var lazyArray = new LazyUint8Array(); - Object.defineProperties(lazyArray, { - length: { - get: /** @this{Object} */ function() { - if (!this.lengthKnown) { - this.cacheLength(); - } - return this._length; - } - }, - chunkSize: { - get: /** @this{Object} */ function() { - if (!this.lengthKnown) { - this.cacheLength(); - } - return this._chunkSize; - } + get chunkSize() { + if (!this.lengthKnown) { + this.cacheLength(); } - }); - + return this._chunkSize; + } + } + + if (globalThis.XMLHttpRequest) { + if (!ENVIRONMENT_IS_WORKER) abort('Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc'); + var lazyArray = new LazyUint8Array(); var properties = { isDevice: false, contents: lazyArray }; } else { var properties = { isDevice: false, url: url }; } - + var node = FS.createFile(parent, name, properties, canRead, canWrite); // This is a total hack, but I want to get this lazy file code out of the // core of MEMFS. If we want to keep this lazy file concept I feel it should @@ -4247,19 +3987,17 @@ function dbg(text) { // Add a function that defers querying the file size until it is asked the first time. Object.defineProperties(node, { usedBytes: { - get: /** @this {FSNode} */ function() { return this.contents.length; } + get: function() { return this.contents.length; } } }); // override each stream op with one that tries to force load the lazy file first var stream_ops = {}; - var keys = Object.keys(node.stream_ops); - keys.forEach((key) => { - var fn = node.stream_ops[key]; - stream_ops[key] = function forceLoadLazyFile() { + for (const [key, fn] of Object.entries(node.stream_ops)) { + stream_ops[key] = (...args) => { FS.forceLoadFile(node); - return fn.apply(null, arguments); + return fn(...args); }; - }); + } function writeChunks(stream, buffer, offset, length, position) { var contents = stream.node.contents; if (position >= contents.length) @@ -4295,48 +4033,10 @@ function dbg(text) { node.stream_ops = stream_ops; return node; }, - absolutePath() { - abort('FS.absolutePath has been removed; use PATH_FS.resolve instead'); - }, - createFolder() { - abort('FS.createFolder has been removed; use FS.mkdir instead'); - }, - createLink() { - abort('FS.createLink has been removed; use FS.symlink instead'); - }, - joinPath() { - abort('FS.joinPath has been removed; use PATH.join instead'); - }, - mmapAlloc() { - abort('FS.mmapAlloc has been replaced by the top level function mmapAlloc'); - }, - standardizePath() { - abort('FS.standardizePath has been removed; use PATH.normalize instead'); - }, }; - - - /** - * Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the - * emscripten HEAP, returns a copy of that string as a Javascript String object. - * - * @param {number} ptr - * @param {number=} maxBytesToRead - An optional length that specifies the - * maximum number of bytes to read. You can omit this parameter to scan the - * string until the first 0 byte. If maxBytesToRead is passed, and the string - * at [ptr, ptr+maxBytesToReadr[ contains a null byte in the middle, then the - * string will cut short at that byte index (i.e. maxBytesToRead will not - * produce a string of exact length [ptr, ptr+maxBytesToRead[) N.B. mixing - * frequent uses of UTF8ToString() with and without maxBytesToRead may throw - * JS JIT optimizations off, so it is worth to consider consistently using one - * @return {string} - */ - var UTF8ToString = (ptr, maxBytesToRead) => { - assert(typeof ptr == 'number', `UTF8ToString expects a number (got ${typeof ptr})`); - return ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead) : ''; - }; + var SYSCALLS = { - DEFAULT_POLLMASK:5, + currentUmask:18, calculateAt(dirfd, path, allowEmpty) { if (PATH.isAbs(path)) { return path; @@ -4355,39 +4055,42 @@ function dbg(text) { } return dir; } - return PATH.join2(dir, path); + return dir + '/' + path; }, - doStat(func, path, buf) { - try { - var stat = func(path); - } catch (e) { - if (e && e.node && PATH.normalize(path) !== PATH.normalize(FS.getPath(e.node))) { - // an error occurred while trying to look up the path; we should just report ENOTDIR - return -54; - } - throw e; - } - HEAP32[((buf)>>2)] = stat.dev; - HEAP32[(((buf)+(4))>>2)] = stat.mode; + writeStat(buf, stat) { + HEAPU32[((buf)>>2)] = stat.dev; + HEAPU32[(((buf)+(4))>>2)] = stat.mode; HEAPU32[(((buf)+(8))>>2)] = stat.nlink; - HEAP32[(((buf)+(12))>>2)] = stat.uid; - HEAP32[(((buf)+(16))>>2)] = stat.gid; - HEAP32[(((buf)+(20))>>2)] = stat.rdev; - (tempI64 = [stat.size>>>0,(tempDouble = stat.size,(+(Math.abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? (+(Math.floor((tempDouble)/4294967296.0)))>>>0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)], HEAP32[(((buf)+(24))>>2)] = tempI64[0],HEAP32[(((buf)+(28))>>2)] = tempI64[1]); + HEAPU32[(((buf)+(12))>>2)] = stat.uid; + HEAPU32[(((buf)+(16))>>2)] = stat.gid; + HEAPU32[(((buf)+(20))>>2)] = stat.rdev; + HEAP64[(((buf)+(24))>>3)] = BigInt(stat.size); HEAP32[(((buf)+(32))>>2)] = 4096; HEAP32[(((buf)+(36))>>2)] = stat.blocks; var atime = stat.atime.getTime(); var mtime = stat.mtime.getTime(); var ctime = stat.ctime.getTime(); - (tempI64 = [Math.floor(atime / 1000)>>>0,(tempDouble = Math.floor(atime / 1000),(+(Math.abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? (+(Math.floor((tempDouble)/4294967296.0)))>>>0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)], HEAP32[(((buf)+(40))>>2)] = tempI64[0],HEAP32[(((buf)+(44))>>2)] = tempI64[1]); - HEAPU32[(((buf)+(48))>>2)] = (atime % 1000) * 1000; - (tempI64 = [Math.floor(mtime / 1000)>>>0,(tempDouble = Math.floor(mtime / 1000),(+(Math.abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? (+(Math.floor((tempDouble)/4294967296.0)))>>>0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)], HEAP32[(((buf)+(56))>>2)] = tempI64[0],HEAP32[(((buf)+(60))>>2)] = tempI64[1]); - HEAPU32[(((buf)+(64))>>2)] = (mtime % 1000) * 1000; - (tempI64 = [Math.floor(ctime / 1000)>>>0,(tempDouble = Math.floor(ctime / 1000),(+(Math.abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? (+(Math.floor((tempDouble)/4294967296.0)))>>>0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)], HEAP32[(((buf)+(72))>>2)] = tempI64[0],HEAP32[(((buf)+(76))>>2)] = tempI64[1]); - HEAPU32[(((buf)+(80))>>2)] = (ctime % 1000) * 1000; - (tempI64 = [stat.ino>>>0,(tempDouble = stat.ino,(+(Math.abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? (+(Math.floor((tempDouble)/4294967296.0)))>>>0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)], HEAP32[(((buf)+(88))>>2)] = tempI64[0],HEAP32[(((buf)+(92))>>2)] = tempI64[1]); + HEAP64[(((buf)+(40))>>3)] = BigInt(Math.floor(atime / 1000)); + HEAPU32[(((buf)+(48))>>2)] = (atime % 1000) * 1000 * 1000; + HEAP64[(((buf)+(56))>>3)] = BigInt(Math.floor(mtime / 1000)); + HEAPU32[(((buf)+(64))>>2)] = (mtime % 1000) * 1000 * 1000; + HEAP64[(((buf)+(72))>>3)] = BigInt(Math.floor(ctime / 1000)); + HEAPU32[(((buf)+(80))>>2)] = (ctime % 1000) * 1000 * 1000; + HEAP64[(((buf)+(88))>>3)] = BigInt(stat.ino); return 0; }, + writeStatFs(buf, stats) { + HEAPU32[(((buf)+(4))>>2)] = stats.bsize; + HEAPU32[(((buf)+(60))>>2)] = stats.bsize; + HEAP64[(((buf)+(8))>>3)] = BigInt(stats.blocks); + HEAP64[(((buf)+(16))>>3)] = BigInt(stats.bfree); + HEAP64[(((buf)+(24))>>3)] = BigInt(stats.bavail); + HEAP64[(((buf)+(32))>>3)] = BigInt(stats.files); + HEAP64[(((buf)+(40))>>3)] = BigInt(stats.ffree); + HEAPU32[(((buf)+(48))>>2)] = stats.fsid; + HEAPU32[(((buf)+(64))>>2)] = stats.flags; // ST_NOSUID + HEAPU32[(((buf)+(56))>>2)] = stats.namelen; + }, doMsync(addr, stream, len, flags, offset) { if (!FS.isFile(stream.node.mode)) { throw new FS.ErrnoError(43); @@ -4399,29 +4102,21 @@ function dbg(text) { var buffer = HEAPU8.slice(addr, addr + len); FS.msync(stream, buffer, offset, len, flags); }, - varargs:undefined, - get() { - assert(SYSCALLS.varargs != undefined); - // the `+` prepended here is necessary to convince the JSCompiler that varargs is indeed a number. - var ret = HEAP32[((+SYSCALLS.varargs)>>2)]; - SYSCALLS.varargs += 4; - return ret; + getStreamFromFD(fd) { + var stream = FS.getStreamChecked(fd); + return stream; }, - getp() { return SYSCALLS.get() }, + varargs:undefined, getStr(ptr) { var ret = UTF8ToString(ptr); return ret; }, - getStreamFromFD(fd) { - var stream = FS.getStreamChecked(fd); - return stream; - }, }; function ___syscall_faccessat(dirfd, path, amode, flags) { try { - + path = SYSCALLS.getStr(path); - assert(flags === 0); + assert(!flags || flags == 512); path = SYSCALLS.calculateAt(dirfd, path); if (amode & ~7) { // need a valid mode @@ -4445,20 +4140,26 @@ function dbg(text) { return -e.errno; } } + - var setErrNo = (value) => { - HEAP32[((___errno_location())>>2)] = value; - return value; + var syscallGetVarargI = () => { + assert(SYSCALLS.varargs != undefined); + // the `+` prepended here is necessary to convince the JSCompiler that varargs is indeed a number. + var ret = HEAP32[((+SYSCALLS.varargs)>>2)]; + SYSCALLS.varargs += 4; + return ret; }; - + var syscallGetVarargP = syscallGetVarargI; + + function ___syscall_fcntl64(fd, cmd, varargs) { SYSCALLS.varargs = varargs; try { - + var stream = SYSCALLS.getStreamFromFD(fd); switch (cmd) { case 0: { - var arg = SYSCALLS.get(); + var arg = syscallGetVarargI(); if (arg < 0) { return -28; } @@ -4466,7 +4167,7 @@ function dbg(text) { arg++; } var newStream; - newStream = FS.createStream(stream, arg); + newStream = FS.dupStream(stream, arg); return newStream.fd; } case 1: @@ -4475,57 +4176,53 @@ function dbg(text) { case 3: return stream.flags; case 4: { - var arg = SYSCALLS.get(); - stream.flags |= arg; + var arg = syscallGetVarargI(); + var mask = 289792; + stream.flags = (stream.flags & ~mask) | (arg & mask); return 0; } - case 5: { - var arg = SYSCALLS.getp(); + case 12: { + var arg = syscallGetVarargP(); var offset = 0; // We're always unlocked. HEAP16[(((arg)+(offset))>>1)] = 2; return 0; } - case 6: - case 7: - return 0; // Pretend that the locking is successful. - case 16: - case 8: - return -28; // These are for sockets. We don't have them fully implemented yet. - case 9: - // musl trusts getown return values, due to a bug where they must be, as they overlap with errors. just return -1 here, so fcntl() returns that, and we set errno ourselves. - setErrNo(28); - return -1; - default: { - return -28; - } + case 13: + case 14: + // Pretend that the locking is successful. These are process-level locks, + // and Emscripten programs are a single process. If we supported linking a + // filesystem between programs, we'd need to do more here. + // See https://github.com/emscripten-core/emscripten/issues/23697 + return 0; } + return -28; } catch (e) { if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; return -e.errno; } } + function ___syscall_fstat64(fd, buf) { try { - - var stream = SYSCALLS.getStreamFromFD(fd); - return SYSCALLS.doStat(FS.stat, stream.path, buf); + + return SYSCALLS.writeStat(buf, FS.fstat(fd)); } catch (e) { if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; return -e.errno; } } + - + var stringToUTF8 = (str, outPtr, maxBytesToWrite) => { - assert(typeof maxBytesToWrite == 'number', 'stringToUTF8(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!'); + assert(typeof maxBytesToWrite == 'number', 'stringToUTF8 requires a third parameter that specifies the length of the output buffer'); return stringToUTF8Array(str, HEAPU8, outPtr, maxBytesToWrite); }; - function ___syscall_getcwd(buf, size) { try { - + if (size === 0) return -28; var cwd = FS.cwd(); var cwdLengthInBytes = lengthBytesUTF8(cwd) + 1; @@ -4537,11 +4234,13 @@ function dbg(text) { return -e.errno; } } + + function ___syscall_ioctl(fd, op, varargs) { SYSCALLS.varargs = varargs; try { - + var stream = SYSCALLS.getStreamFromFD(fd); switch (op) { case 21509: { @@ -4552,13 +4251,13 @@ function dbg(text) { if (!stream.tty) return -59; if (stream.tty.ops.ioctl_tcgets) { var termios = stream.tty.ops.ioctl_tcgets(stream); - var argp = SYSCALLS.getp(); + var argp = syscallGetVarargP(); HEAP32[((argp)>>2)] = termios.c_iflag || 0; HEAP32[(((argp)+(4))>>2)] = termios.c_oflag || 0; HEAP32[(((argp)+(8))>>2)] = termios.c_cflag || 0; HEAP32[(((argp)+(12))>>2)] = termios.c_lflag || 0; for (var i = 0; i < 32; i++) { - HEAP8[(((argp + i)+(17))>>0)] = termios.c_cc[i] || 0; + HEAP8[(argp + i)+(17)] = termios.c_cc[i] || 0; } return 0; } @@ -4575,14 +4274,14 @@ function dbg(text) { case 21508: { if (!stream.tty) return -59; if (stream.tty.ops.ioctl_tcsets) { - var argp = SYSCALLS.getp(); + var argp = syscallGetVarargP(); var c_iflag = HEAP32[((argp)>>2)]; var c_oflag = HEAP32[(((argp)+(4))>>2)]; var c_cflag = HEAP32[(((argp)+(8))>>2)]; var c_lflag = HEAP32[(((argp)+(12))>>2)]; var c_cc = [] for (var i = 0; i < 32; i++) { - c_cc.push(HEAP8[(((argp + i)+(17))>>0)]); + c_cc.push(HEAP8[(argp + i)+(17)]); } return stream.tty.ops.ioctl_tcsets(stream.tty, op, { c_iflag, c_oflag, c_cflag, c_lflag, c_cc }); } @@ -4590,7 +4289,7 @@ function dbg(text) { } case 21519: { if (!stream.tty) return -59; - var argp = SYSCALLS.getp(); + var argp = syscallGetVarargP(); HEAP32[((argp)>>2)] = 0; return 0; } @@ -4598,8 +4297,9 @@ function dbg(text) { if (!stream.tty) return -59; return -28; // not supported } + case 21537: case 21531: { - var argp = SYSCALLS.getp(); + var argp = syscallGetVarargP(); return FS.ioctl(stream, op, argp); } case 21523: { @@ -4608,7 +4308,7 @@ function dbg(text) { if (!stream.tty) return -59; if (stream.tty.ops.ioctl_tiocgwinsz) { var winsize = stream.tty.ops.ioctl_tiocgwinsz(stream.tty); - var argp = SYSCALLS.getp(); + var argp = syscallGetVarargP(); HEAP16[((argp)>>1)] = winsize[0]; HEAP16[(((argp)+(2))>>1)] = winsize[1]; } @@ -4632,58 +4332,66 @@ function dbg(text) { return -e.errno; } } + function ___syscall_lstat64(path, buf) { try { - + path = SYSCALLS.getStr(path); - return SYSCALLS.doStat(FS.lstat, path, buf); + return SYSCALLS.writeStat(buf, FS.lstat(path)); } catch (e) { if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; return -e.errno; } } + function ___syscall_newfstatat(dirfd, path, buf, flags) { try { - + path = SYSCALLS.getStr(path); var nofollow = flags & 256; var allowEmpty = flags & 4096; flags = flags & (~6400); assert(!flags, `unknown flags in __syscall_newfstatat: ${flags}`); path = SYSCALLS.calculateAt(dirfd, path, allowEmpty); - return SYSCALLS.doStat(nofollow ? FS.lstat : FS.stat, path, buf); + return SYSCALLS.writeStat(buf, nofollow ? FS.lstat(path) : FS.stat(path)); } catch (e) { if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; return -e.errno; } } + + function ___syscall_openat(dirfd, path, flags, varargs) { SYSCALLS.varargs = varargs; try { - + path = SYSCALLS.getStr(path); path = SYSCALLS.calculateAt(dirfd, path); - var mode = varargs ? SYSCALLS.get() : 0; + var mode = varargs ? syscallGetVarargI() : 0; + if (flags & 64) { + mode &= ~SYSCALLS.currentUmask; + } return FS.open(path, flags, mode).fd; } catch (e) { if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; return -e.errno; } } + - - + + function ___syscall_readlinkat(dirfd, path, buf, bufsize) { try { - + path = SYSCALLS.getStr(path); path = SYSCALLS.calculateAt(dirfd, path); if (bufsize <= 0) return -28; var ret = FS.readlink(path); - + var len = Math.min(bufsize, lengthBytesUTF8(ret)); var endChar = HEAP8[buf+len]; stringToUTF8(ret, buf, bufsize+1); @@ -4696,10 +4404,11 @@ function dbg(text) { return -e.errno; } } + function ___syscall_rmdir(path) { try { - + path = SYSCALLS.getStr(path); FS.rmdir(path); return 0; @@ -4708,29 +4417,31 @@ function dbg(text) { return -e.errno; } } + function ___syscall_stat64(path, buf) { try { - + path = SYSCALLS.getStr(path); - return SYSCALLS.doStat(FS.stat, path, buf); + return SYSCALLS.writeStat(buf, FS.stat(path)); } catch (e) { if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; return -e.errno; } } + function ___syscall_unlinkat(dirfd, path, flags) { try { - + path = SYSCALLS.getStr(path); path = SYSCALLS.calculateAt(dirfd, path); - if (flags === 0) { + if (!flags) { FS.unlink(path); } else if (flags === 512) { FS.rmdir(path); } else { - abort('Invalid flags passed to unlinkat'); + return -28; } return 0; } catch (e) { @@ -4738,13 +4449,12 @@ function dbg(text) { return -e.errno; } } + - var _emscripten_date_now = () => Date.now(); - - var _emscripten_memcpy_js = (dest, src, num) => HEAPU8.copyWithin(dest, src, src + num); + var __abort_js = () => + abort('native code called abort()'); - var getHeapMax = () => - HEAPU8.length; + var _emscripten_date_now = () => Date.now(); var abortOnCannotGrowMemory = (requestedSize) => { abort(`Cannot enlarge memory arrays to size ${requestedSize} bytes (OOM). Either (1) compile with -sINITIAL_MEMORY=X with X higher than the current value ${HEAP8.length}, (2) compile with -sALLOW_MEMORY_GROWTH which allows increasing the size at runtime, or (3) if you want malloc to return NULL (0) instead of this abort, compile with -sABORTING_MALLOC=0`); @@ -4758,15 +4468,12 @@ function dbg(text) { var ENV = { }; - - var getExecutableName = () => { - return thisProgram || './this.program'; - }; + + var getExecutableName = () => thisProgram || './this.program'; var getEnvStrings = () => { if (!getEnvStrings.strings) { // Default values. - // Browser language detection #8751 - var lang = ((typeof navigator == 'object' && navigator.languages && navigator.languages[0]) || 'C').replace('-', '_') + '.UTF-8'; + var lang = (globalThis.navigator?.language ?? 'C').replace('-', '_') + '.UTF-8'; var env = { 'USER': 'web_user', 'LOGNAME': 'web_user', @@ -4792,70 +4499,63 @@ function dbg(text) { } return getEnvStrings.strings; }; - - var stringToAscii = (str, buffer) => { - for (var i = 0; i < str.length; ++i) { - assert(str.charCodeAt(i) === (str.charCodeAt(i) & 0xff)); - HEAP8[((buffer++)>>0)] = str.charCodeAt(i); - } - // Null-terminate the string - HEAP8[((buffer)>>0)] = 0; - }; - + var _environ_get = (__environ, environ_buf) => { var bufSize = 0; - getEnvStrings().forEach((string, i) => { + var envp = 0; + for (var string of getEnvStrings()) { var ptr = environ_buf + bufSize; - HEAPU32[(((__environ)+(i*4))>>2)] = ptr; - stringToAscii(string, ptr); - bufSize += string.length + 1; - }); + HEAPU32[(((__environ)+(envp))>>2)] = ptr; + bufSize += stringToUTF8(string, ptr, Infinity) + 1; + envp += 4; + } return 0; }; - + var _environ_sizes_get = (penviron_count, penviron_buf_size) => { var strings = getEnvStrings(); HEAPU32[((penviron_count)>>2)] = strings.length; var bufSize = 0; - strings.forEach((string) => bufSize += string.length + 1); + for (var string of strings) { + bufSize += lengthBytesUTF8(string) + 1; + } HEAPU32[((penviron_buf_size)>>2)] = bufSize; return 0; }; - + var runtimeKeepaliveCounter = 0; var keepRuntimeAlive = () => noExitRuntime || runtimeKeepaliveCounter > 0; - var _proc_exit = (code) => { EXITSTATUS = code; if (!keepRuntimeAlive()) { - if (Module['onExit']) Module['onExit'](code); + Module['onExit']?.(code); ABORT = true; } quit_(code, new ExitStatus(code)); }; - - /** @suppress {duplicate } */ + + /** @param {boolean|number=} implicit */ var exitJS = (status, implicit) => { EXITSTATUS = status; - + checkUnflushedContent(); - + // if exit() was called explicitly, warn the user if the runtime isn't actually being shut down if (keepRuntimeAlive() && !implicit) { var msg = `program exited (with status: ${status}), but keepRuntimeAlive() is set (counter=${runtimeKeepaliveCounter}) due to an async operation, so halting execution but not exiting the runtime or preventing further async execution (you can use emscripten_force_exit, if you want to force a true shutdown)`; err(msg); } - + _proc_exit(status); }; var _exit = exitJS; function _fd_close(fd) { try { - + var stream = SYSCALLS.getStreamFromFD(fd); FS.close(stream); return 0; @@ -4864,6 +4564,34 @@ function dbg(text) { return e.errno; } } + + + function _fd_fdstat_get(fd, pbuf) { + try { + + var rightsBase = 0; + var rightsInheriting = 0; + var flags = 0; + { + var stream = SYSCALLS.getStreamFromFD(fd); + // All character devices are terminals (other things a Linux system would + // assume is a character device, like the mouse, we have special APIs for). + var type = stream.tty ? 2 : + FS.isDir(stream.mode) ? 3 : + FS.isLink(stream.mode) ? 7 : + 4; + } + HEAP8[pbuf] = type; + HEAP16[(((pbuf)+(2))>>1)] = flags; + HEAP64[(((pbuf)+(8))>>3)] = BigInt(rightsBase); + HEAP64[(((pbuf)+(16))>>3)] = BigInt(rightsInheriting); + return 0; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return e.errno; + } + } + /** @param {number=} offset */ var doReadv = (stream, iov, iovcnt, offset) => { @@ -4876,16 +4604,16 @@ function dbg(text) { if (curr < 0) return -1; ret += curr; if (curr < len) break; // nothing more to read - if (typeof offset !== 'undefined') { + if (typeof offset != 'undefined') { offset += curr; } } return ret; }; - + function _fd_read(fd, iov, iovcnt, pnum) { try { - + var stream = SYSCALLS.getStreamFromFD(fd); var num = doReadv(stream, iov, iovcnt); HEAPU32[((pnum)>>2)] = num; @@ -4895,23 +4623,23 @@ function dbg(text) { return e.errno; } } - - - var convertI32PairToI53Checked = (lo, hi) => { - assert(lo == (lo >>> 0) || lo == (lo|0)); // lo should either be a i32 or a u32 - assert(hi === (hi|0)); // hi should be a i32 - return ((hi + 0x200000) >>> 0 < 0x400001 - !!lo) ? (lo >>> 0) + hi * 4294967296 : NaN; - }; - function _fd_seek(fd,offset_low, offset_high,whence,newOffset) { - var offset = convertI32PairToI53Checked(offset_low, offset_high);; - - + + + + var INT53_MAX = 9007199254740992; + + var INT53_MIN = -9007199254740992; + var bigintToI53Checked = (num) => (num < INT53_MIN || num > INT53_MAX) ? NaN : Number(num); + function _fd_seek(fd, offset, whence, newOffset) { + offset = bigintToI53Checked(offset); + + try { - - if (isNaN(offset)) return 61; + + if (isNaN(offset)) return 22; var stream = SYSCALLS.getStreamFromFD(fd); FS.llseek(stream, offset, whence); - (tempI64 = [stream.position>>>0,(tempDouble = stream.position,(+(Math.abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? (+(Math.floor((tempDouble)/4294967296.0)))>>>0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)], HEAP32[((newOffset)>>2)] = tempI64[0],HEAP32[(((newOffset)+(4))>>2)] = tempI64[1]); + HEAP64[((newOffset)>>3)] = BigInt(stream.position); if (stream.getdents && offset === 0 && whence === 0) stream.getdents = null; // reset readdir state return 0; } catch (e) { @@ -4931,16 +4659,20 @@ function dbg(text) { var curr = FS.write(stream, HEAP8, ptr, len, offset); if (curr < 0) return -1; ret += curr; - if (typeof offset !== 'undefined') { + if (curr < len) { + // No more space to write. + break; + } + if (typeof offset != 'undefined') { offset += curr; } } return ret; }; - + function _fd_write(fd, iov, iovcnt, pnum) { try { - + var stream = SYSCALLS.getStreamFromFD(fd); var num = doWritev(stream, iov, iovcnt); HEAPU32[((pnum)>>2)] = num; @@ -4950,6 +4682,7 @@ function dbg(text) { return e.errno; } } + var handleException = (e) => { @@ -4970,7 +4703,9 @@ function dbg(text) { quit_(1, e); }; - + + + var stackAlloc = (sz) => __emscripten_stack_alloc(sz); var stringToUTF8OnStack = (str) => { var size = lengthBytesUTF8(str) + 1; var ret = stackAlloc(size); @@ -4980,159 +4715,121 @@ function dbg(text) { + var FS_createPath = (...args) => FS.createPath(...args); + + + + var FS_unlink = (...args) => FS.unlink(...args); + + var FS_createLazyFile = (...args) => FS.createLazyFile(...args); + + var FS_createDevice = (...args) => FS.createDevice(...args); - var FS_unlink = (path) => FS.unlink(path); - var FSNode = /** @constructor */ function(parent, name, mode, rdev) { - if (!parent) { - parent = this; // root node sets parent to itself - } - this.parent = parent; - this.mount = parent.mount; - this.mounted = null; - this.id = FS.nextInode++; - this.name = name; - this.mode = mode; - this.node_ops = {}; - this.stream_ops = {}; - this.rdev = rdev; - }; - var readMode = 292/*292*/ | 73/*73*/; - var writeMode = 146/*146*/; - Object.defineProperties(FSNode.prototype, { - read: { - get: /** @this{FSNode} */function() { - return (this.mode & readMode) === readMode; - }, - set: /** @this{FSNode} */function(val) { - val ? this.mode |= readMode : this.mode &= ~readMode; - } - }, - write: { - get: /** @this{FSNode} */function() { - return (this.mode & writeMode) === writeMode; - }, - set: /** @this{FSNode} */function(val) { - val ? this.mode |= writeMode : this.mode &= ~writeMode; - } - }, - isFolder: { - get: /** @this{FSNode} */function() { - return FS.isDir(this.mode); - } - }, - isDevice: { - get: /** @this{FSNode} */function() { - return FS.isChrdev(this.mode); - } - } - }); - FS.FSNode = FSNode; FS.createPreloadedFile = FS_createPreloadedFile; - FS.staticInit();Module["FS_createPath"] = FS.createPath;Module["FS_createDataFile"] = FS.createDataFile;Module["FS_createPreloadedFile"] = FS.createPreloadedFile;Module["FS_unlink"] = FS.unlink;Module["FS_createLazyFile"] = FS.createLazyFile;Module["FS_createDevice"] = FS.createDevice;; + FS.preloadFile = FS_preloadFile; + FS.staticInit();; if (ENVIRONMENT_IS_NODE) { NODEFS.staticInit(); }; - if (ENVIRONMENT_IS_NODE) { - var _wrapNodeError = function(func) { - return function() { - try { - return func.apply(this, arguments) - } catch (e) { - if (e.code) { - throw new FS.ErrnoError(ERRNO_CODES[e.code]); - } - throw e; + if (!ENVIRONMENT_IS_NODE) { + throw new Error("NODERAWFS is currently only supported on Node.js environment.") + } + var nodeTTY = require('node:tty'); + function _wrapNodeError(func) { + return (...args) => { + try { + return func(...args) + } catch (e) { + // Hack for Deno which throws BadResource instead of EBADF: + // https://github.com/emscripten-core/emscripten/issues/26239 + if (e.name == 'BadResource') { + e.code = 'EBADF'; + } + if (e.code) { + throw new FS.ErrnoError(ERRNO_CODES[e.code]); } + throw e; } - }; - var VFS = Object.assign({}, FS); - for (var _key in NODERAWFS) { - FS[_key] = _wrapNodeError(NODERAWFS[_key]); } - } else { - throw new Error("NODERAWFS is currently only supported on Node.js environment.") + } + function _wrapNodeStreamFunc(func, vfs_func) { + return _wrapNodeError((stream, ...args) => { + if (stream.stream_ops) { + // this stream was created by some other FS. e.g: PIPEFS. + return vfs_func(stream, ...args); + } + return func(stream, ...args); + }); + } + // Use this to reference our in-memory filesystem + /** @suppress {partialAlias} */ + var VFS = {...FS}; + // Wrap the whole in-memory filesystem API with + // our Node.js based functions + for (const [key, value] of Object.entries(NODERAWFS)) { + FS[key] = _wrapNodeError(value); + } + for (const [key, value] of Object.entries(NODERAWFS_stream_funcs)) { + FS[key] = _wrapNodeStreamFunc(value, FS[key]); }; -function checkIncomingModuleAPI() { - ignoredModuleProp('fetchSettings'); -} -var wasmImports = { - /** @export */ - __syscall_faccessat: ___syscall_faccessat, - /** @export */ - __syscall_fcntl64: ___syscall_fcntl64, - /** @export */ - __syscall_fstat64: ___syscall_fstat64, - /** @export */ - __syscall_getcwd: ___syscall_getcwd, - /** @export */ - __syscall_ioctl: ___syscall_ioctl, - /** @export */ - __syscall_lstat64: ___syscall_lstat64, - /** @export */ - __syscall_newfstatat: ___syscall_newfstatat, - /** @export */ - __syscall_openat: ___syscall_openat, - /** @export */ - __syscall_readlinkat: ___syscall_readlinkat, - /** @export */ - __syscall_rmdir: ___syscall_rmdir, - /** @export */ - __syscall_stat64: ___syscall_stat64, - /** @export */ - __syscall_unlinkat: ___syscall_unlinkat, - /** @export */ - emscripten_date_now: _emscripten_date_now, - /** @export */ - emscripten_memcpy_js: _emscripten_memcpy_js, - /** @export */ - emscripten_resize_heap: _emscripten_resize_heap, - /** @export */ - environ_get: _environ_get, - /** @export */ - environ_sizes_get: _environ_sizes_get, - /** @export */ - exit: _exit, - /** @export */ - fd_close: _fd_close, - /** @export */ - fd_read: _fd_read, - /** @export */ - fd_seek: _fd_seek, - /** @export */ - fd_write: _fd_write -}; -var wasmExports = createWasm(); -var ___wasm_call_ctors = createExportWrapper('__wasm_call_ctors'); -var ___errno_location = createExportWrapper('__errno_location'); -var _main = Module['_main'] = createExportWrapper('__main_argc_argv'); -var _fflush = Module['_fflush'] = createExportWrapper('fflush'); -var _malloc = createExportWrapper('malloc'); -var _free = createExportWrapper('free'); -var _emscripten_stack_init = () => (_emscripten_stack_init = wasmExports['emscripten_stack_init'])(); -var _emscripten_stack_get_free = () => (_emscripten_stack_get_free = wasmExports['emscripten_stack_get_free'])(); -var _emscripten_stack_get_base = () => (_emscripten_stack_get_base = wasmExports['emscripten_stack_get_base'])(); -var _emscripten_stack_get_end = () => (_emscripten_stack_get_end = wasmExports['emscripten_stack_get_end'])(); -var stackSave = createExportWrapper('stackSave'); -var stackRestore = createExportWrapper('stackRestore'); -var stackAlloc = createExportWrapper('stackAlloc'); -var _emscripten_stack_get_current = () => (_emscripten_stack_get_current = wasmExports['emscripten_stack_get_current'])(); -var dynCall_jiji = Module['dynCall_jiji'] = createExportWrapper('dynCall_jiji'); +// End JS library code +// include: postlibrary.js +// This file is included after the automatically-generated JS library code +// but before the wasm module is created. -// include: postamble.js -// === Auto-generated postamble setup entry stuff === +{ + + // Begin ATMODULES hooks + if (Module['noExitRuntime']) noExitRuntime = Module['noExitRuntime']; +if (Module['preloadPlugins']) preloadPlugins = Module['preloadPlugins']; +if (Module['print']) out = Module['print']; +if (Module['printErr']) err = Module['printErr']; +if (Module['wasmBinary']) wasmBinary = Module['wasmBinary']; + // End ATMODULES hooks + + checkIncomingModuleAPI(); + + if (Module['arguments']) arguments_ = Module['arguments']; + if (Module['thisProgram']) thisProgram = Module['thisProgram']; + + // Assertions on removed incoming Module JS APIs. + assert(typeof Module['memoryInitializerPrefixURL'] == 'undefined', 'Module.memoryInitializerPrefixURL option was removed, use Module.locateFile instead'); + assert(typeof Module['pthreadMainPrefixURL'] == 'undefined', 'Module.pthreadMainPrefixURL option was removed, use Module.locateFile instead'); + assert(typeof Module['cdInitializerPrefixURL'] == 'undefined', 'Module.cdInitializerPrefixURL option was removed, use Module.locateFile instead'); + assert(typeof Module['filePackagePrefixURL'] == 'undefined', 'Module.filePackagePrefixURL option was removed, use Module.locateFile instead'); + assert(typeof Module['read'] == 'undefined', 'Module.read option was removed'); + assert(typeof Module['readAsync'] == 'undefined', 'Module.readAsync option was removed (modify readAsync in JS)'); + assert(typeof Module['readBinary'] == 'undefined', 'Module.readBinary option was removed (modify readBinary in JS)'); + assert(typeof Module['setWindowTitle'] == 'undefined', 'Module.setWindowTitle option was removed (modify emscripten_set_window_title in JS)'); + assert(typeof Module['TOTAL_MEMORY'] == 'undefined', 'Module.TOTAL_MEMORY has been renamed Module.INITIAL_MEMORY'); + assert(typeof Module['ENVIRONMENT'] == 'undefined', 'Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -sENVIRONMENT=web or -sENVIRONMENT=node)'); + assert(typeof Module['STACK_SIZE'] == 'undefined', 'STACK_SIZE can no longer be set at runtime. Use -sSTACK_SIZE at link time') + // If memory is defined in wasm, the user can't provide it, or set INITIAL_MEMORY + assert(typeof Module['wasmMemory'] == 'undefined', 'Use of `wasmMemory` detected. Use -sIMPORTED_MEMORY to define wasmMemory externally'); + assert(typeof Module['INITIAL_MEMORY'] == 'undefined', 'Detected runtime INITIAL_MEMORY setting. Use -sIMPORTED_MEMORY to define wasmMemory dynamically'); + + if (Module['preInit']) { + if (typeof Module['preInit'] == 'function') Module['preInit'] = [Module['preInit']]; + while (Module['preInit'].length > 0) { + Module['preInit'].shift()(); + } + } + consumedModuleProp('preInit'); +} -Module['addRunDependency'] = addRunDependency; -Module['removeRunDependency'] = removeRunDependency; -Module['FS_createPath'] = FS.createPath; -Module['FS_createLazyFile'] = FS.createLazyFile; -Module['FS_createDevice'] = FS.createDevice; -Module['FS_createPreloadedFile'] = FS.createPreloadedFile; -Module['FS'] = FS; -Module['FS_createDataFile'] = FS.createDataFile; -Module['FS_unlink'] = FS.unlink; -var missingLibrarySymbols = [ +// Begin runtime exports + Module['addRunDependency'] = addRunDependency; + Module['removeRunDependency'] = removeRunDependency; + Module['FS_preloadFile'] = FS_preloadFile; + Module['FS_unlink'] = FS_unlink; + Module['FS_createPath'] = FS_createPath; + Module['FS_createDevice'] = FS_createDevice; + Module['FS'] = FS; + Module['FS_createDataFile'] = FS_createDataFile; + Module['FS_createLazyFile'] = FS_createLazyFile; + var missingLibrarySymbols = [ 'writeI53ToI64', 'writeI53ToI64Clamped', 'writeI53ToI64Signaling', @@ -5141,28 +4838,24 @@ var missingLibrarySymbols = [ 'readI53FromI64', 'readI53FromU64', 'convertI32PairToI53', + 'convertI32PairToI53Checked', 'convertU32PairToI53', + 'getTempRet0', + 'setTempRet0', + 'createNamedFunction', + 'zeroMemory', + 'getHeapMax', 'growMemory', - 'isLeapYear', - 'ydayFromDate', - 'arraySum', - 'addDays', + 'withStackSave', 'inetPton4', 'inetNtop4', 'inetPton6', 'inetNtop6', 'readSockaddr', 'writeSockaddr', - 'getHostByName', - 'getCallstack', - 'emscriptenLog', - 'convertPCtoSourceLocation', 'readEmAsmArgs', 'jstoi_q', - 'jstoi_s', - 'listenOnce', 'autoResumeAudioContext', - 'dynCallLegacy', 'getDynCaller', 'dynCall', 'runtimeKeepalivePush', @@ -5170,32 +4863,27 @@ var missingLibrarySymbols = [ 'callUserCallback', 'maybeExit', 'asmjsMangle', - 'handleAllocatorInit', + 'alignMemory', 'HandleAllocator', - 'getNativeTypeSize', + 'addOnInit', + 'addOnPostCtor', + 'addOnPreMain', + 'addOnExit', 'STACK_SIZE', 'STACK_ALIGN', 'POINTER_SIZE', 'ASSERTIONS', - 'getCFunc', 'ccall', 'cwrap', - 'uleb128Encode', - 'sigToWasmTypes', - 'generateFuncType', 'convertJsFunctionToWasm', 'getEmptyTableSlot', 'updateTableMap', 'getFunctionAddress', 'addFunction', 'removeFunction', - 'reallyNegative', - 'unSign', - 'strLen', - 'reSign', - 'formatString', 'intArrayToString', 'AsciiToString', + 'stringToAscii', 'UTF16ToString', 'stringToUTF16', 'lengthBytesUTF16', @@ -5207,7 +4895,6 @@ var missingLibrarySymbols = [ 'registerKeyEventCallback', 'maybeCStringToJsString', 'findEventTarget', - 'findCanvasEventTarget', 'getBoundingClientRect', 'fillMouseEventData', 'registerMouseEventCallback', @@ -5242,42 +4929,51 @@ var missingLibrarySymbols = [ 'registerGamepadEventCallback', 'registerBeforeUnloadEventCallback', 'fillBatteryEventData', - 'battery', 'registerBatteryEventCallback', 'setCanvasElementSize', 'getCanvasElementSize', 'jsStackTrace', - 'stackTrace', + 'getCallstack', + 'convertPCtoSourceLocation', 'checkWasiClock', 'wasiRightsToMuslOFlags', 'wasiOFlagsToMuslOFlags', - 'createDyncallWrapper', 'safeSetTimeout', 'setImmediateWrapped', + 'safeRequestAnimationFrame', 'clearImmediateWrapped', - 'polyfillSetImmediate', + 'registerPostMainLoop', + 'registerPreMainLoop', 'getPromise', 'makePromise', 'idsToPromises', 'makePromiseCallback', 'ExceptionInfo', 'findMatchingCatch', - 'setMainLoop', + 'incrementUncaughtExceptionCount', + 'decrementUncaughtExceptionCount', + 'Browser_asyncPrepareDataCounter', + 'isLeapYear', + 'ydayFromDate', + 'arraySum', + 'addDays', 'getSocketFromFD', 'getSocketAddress', 'FS_mkdirTree', '_setNetworkCallback', 'heapObjectForWebGLType', - 'heapAccessShiftForWebGLHeap', + 'toTypedArrayIndex', 'webgl_enable_ANGLE_instanced_arrays', 'webgl_enable_OES_vertex_array_object', 'webgl_enable_WEBGL_draw_buffers', 'webgl_enable_WEBGL_multi_draw', + 'webgl_enable_EXT_polygon_offset_clamp', + 'webgl_enable_EXT_clip_control', + 'webgl_enable_WEBGL_polygon_mode', 'emscriptenWebGLGet', 'computeUnpackAlignedImageSize', 'colorChannelsInGlTextureFormat', 'emscriptenWebGLGetTexPixelData', - '__glGenObject', 'emscriptenWebGLGetUniform', 'webglGetUniformLocation', 'webglPrepareUniformLocationsBeforeFirstUse', @@ -5287,71 +4983,67 @@ var missingLibrarySymbols = [ 'writeGLArray', 'registerWebGlEventCallback', 'runAndAbortIfError', - 'SDL_unicode', - 'SDL_ttfContext', - 'SDL_audio', 'ALLOC_NORMAL', 'ALLOC_STACK', 'allocate', 'writeStringToMemory', 'writeAsciiToMemory', + 'allocateUTF8', + 'allocateUTF8OnStack', + 'demangle', + 'stackTrace', + 'getNativeTypeSize', ]; missingLibrarySymbols.forEach(missingLibrarySymbol) -var unexportedSymbols = [ + var unexportedSymbols = [ 'run', - 'addOnPreRun', - 'addOnInit', - 'addOnPreMain', - 'addOnExit', - 'addOnPostRun', - 'FS_createFolder', - 'FS_createLink', - 'FS_readFile', 'out', 'err', 'callMain', 'abort', - 'wasmMemory', 'wasmExports', - 'stackAlloc', - 'stackSave', - 'stackRestore', - 'getTempRet0', - 'setTempRet0', 'writeStackCookie', 'checkStackCookie', - 'convertI32PairToI53Checked', + 'INT53_MAX', + 'INT53_MIN', + 'bigintToI53Checked', + 'HEAP8', + 'HEAPU8', + 'HEAP16', + 'HEAPU16', + 'HEAP32', + 'HEAPU32', + 'HEAPF32', + 'HEAPF64', + 'HEAP64', + 'HEAPU64', + 'stackSave', + 'stackRestore', + 'stackAlloc', 'ptrToString', - 'zeroMemory', 'exitJS', - 'getHeapMax', 'abortOnCannotGrowMemory', 'ENV', - 'MONTH_DAYS_REGULAR', - 'MONTH_DAYS_LEAP', - 'MONTH_DAYS_REGULAR_CUMULATIVE', - 'MONTH_DAYS_LEAP_CUMULATIVE', 'ERRNO_CODES', - 'ERRNO_MESSAGES', - 'setErrNo', + 'strError', 'DNS', 'Protocols', 'Sockets', - 'initRandomFill', - 'randomFill', 'timers', 'warnOnce', - 'UNWIND_CACHE', 'readEmAsmArgsArray', 'getExecutableName', 'handleException', 'keepRuntimeAlive', 'asyncLoad', - 'alignMemory', 'mmapAlloc', 'wasmTable', + 'wasmMemory', + 'getUniqueRunDependency', 'noExitRuntime', + 'addOnPreRun', + 'addOnPostRun', 'freeTableIndexes', 'functionsInTableMap', 'setValue', @@ -5365,31 +5057,150 @@ var unexportedSymbols = [ 'stringToUTF8', 'lengthBytesUTF8', 'intArrayFromString', - 'stringToAscii', 'UTF16Decoder', 'stringToUTF8OnStack', 'JSEvents', 'specialHTMLTargets', + 'findCanvasEventTarget', 'currentFullscreenStrategy', 'restoreOldWindowedStyle', - 'demangle', - 'demangleAll', + 'UNWIND_CACHE', 'ExitStatus', 'getEnvStrings', 'doReadv', 'doWritev', + 'initRandomFill', + 'randomFill', + 'emSetImmediate', + 'emClearImmediate_deps', + 'emClearImmediate', 'promiseMap', 'uncaughtExceptionCount', - 'exceptionLast', 'exceptionCaught', 'Browser', + 'requestFullscreen', + 'requestFullScreen', + 'setCanvasSize', + 'getUserMedia', + 'createContext', + 'getPreloadedImageData__data', 'wget', + 'MONTH_DAYS_REGULAR', + 'MONTH_DAYS_LEAP', + 'MONTH_DAYS_REGULAR_CUMULATIVE', + 'MONTH_DAYS_LEAP_CUMULATIVE', 'SYSCALLS', 'preloadPlugins', + 'FS_createPreloadedFile', 'FS_modeStringToFlags', 'FS_getMode', + 'FS_fileDataToTypedArray', 'FS_stdin_getChar_buffer', 'FS_stdin_getChar', + 'FS_readFile', + 'FS_root', + 'FS_mounts', + 'FS_devices', + 'FS_streams', + 'FS_nextInode', + 'FS_nameTable', + 'FS_currentPath', + 'FS_initialized', + 'FS_ignorePermissions', + 'FS_filesystems', + 'FS_syncFSRequests', + 'FS_lookupPath', + 'FS_getPath', + 'FS_hashName', + 'FS_hashAddNode', + 'FS_hashRemoveNode', + 'FS_lookupNode', + 'FS_createNode', + 'FS_destroyNode', + 'FS_isRoot', + 'FS_isMountpoint', + 'FS_isFile', + 'FS_isDir', + 'FS_isLink', + 'FS_isChrdev', + 'FS_isBlkdev', + 'FS_isFIFO', + 'FS_isSocket', + 'FS_flagsToPermissionString', + 'FS_nodePermissions', + 'FS_mayLookup', + 'FS_mayCreate', + 'FS_mayDelete', + 'FS_mayOpen', + 'FS_checkOpExists', + 'FS_nextfd', + 'FS_getStreamChecked', + 'FS_getStream', + 'FS_createStream', + 'FS_closeStream', + 'FS_dupStream', + 'FS_doSetAttr', + 'FS_chrdev_stream_ops', + 'FS_major', + 'FS_minor', + 'FS_makedev', + 'FS_registerDevice', + 'FS_getDevice', + 'FS_getMounts', + 'FS_syncfs', + 'FS_mount', + 'FS_unmount', + 'FS_lookup', + 'FS_mknod', + 'FS_statfs', + 'FS_statfsStream', + 'FS_statfsNode', + 'FS_create', + 'FS_mkdir', + 'FS_mkdev', + 'FS_symlink', + 'FS_rename', + 'FS_rmdir', + 'FS_readdir', + 'FS_readlink', + 'FS_stat', + 'FS_fstat', + 'FS_lstat', + 'FS_doChmod', + 'FS_chmod', + 'FS_lchmod', + 'FS_fchmod', + 'FS_doChown', + 'FS_chown', + 'FS_lchown', + 'FS_fchown', + 'FS_doTruncate', + 'FS_truncate', + 'FS_ftruncate', + 'FS_utime', + 'FS_open', + 'FS_close', + 'FS_isClosed', + 'FS_llseek', + 'FS_read', + 'FS_write', + 'FS_mmap', + 'FS_msync', + 'FS_ioctl', + 'FS_writeFile', + 'FS_cwd', + 'FS_chdir', + 'FS_createDefaultDirectories', + 'FS_createDefaultDevices', + 'FS_createSpecialDirectories', + 'FS_createStandardStreams', + 'FS_staticInit', + 'FS_init', + 'FS_quit', + 'FS_findObject', + 'FS_analyzePath', + 'FS_createFile', + 'FS_forceLoadFile', 'MEMFS', 'TTY', 'PIPEFS', @@ -5398,7 +5209,6 @@ var unexportedSymbols = [ 'miniTempWebGLFloatBuffers', 'miniTempWebGLIntBuffers', 'GL', - 'emscripten_webgl_power_preferences', 'AL', 'GLUT', 'EGL', @@ -5406,26 +5216,131 @@ var unexportedSymbols = [ 'IDBStore', 'SDL', 'SDL_gfx', - 'allocateUTF8', - 'allocateUTF8OnStack', + 'print', + 'printErr', + 'jstoi_s', 'NODEFS', 'NODERAWFS', + 'NODERAWFS_stream_funcs', ]; unexportedSymbols.forEach(unexportedRuntimeSymbol); + // End runtime exports + // Begin JS library exports + // End JS library exports +// end include: postlibrary.js -var calledRun; +function checkIncomingModuleAPI() { + ignoredModuleProp('fetchSettings'); + ignoredModuleProp('logReadFiles'); + ignoredModuleProp('loadSplitModule'); + ignoredModuleProp('onMalloc'); + ignoredModuleProp('onRealloc'); + ignoredModuleProp('onFree'); + ignoredModuleProp('onSbrkGrow'); +} + +// Imports from the Wasm binary. +var _strerror = makeInvalidEarlyAccess('_strerror'); +var _main = Module['_main'] = makeInvalidEarlyAccess('_main'); +var _fflush = makeInvalidEarlyAccess('_fflush'); +var _emscripten_stack_get_end = makeInvalidEarlyAccess('_emscripten_stack_get_end'); +var _emscripten_stack_get_base = makeInvalidEarlyAccess('_emscripten_stack_get_base'); +var _emscripten_stack_init = makeInvalidEarlyAccess('_emscripten_stack_init'); +var _emscripten_stack_get_free = makeInvalidEarlyAccess('_emscripten_stack_get_free'); +var __emscripten_stack_restore = makeInvalidEarlyAccess('__emscripten_stack_restore'); +var __emscripten_stack_alloc = makeInvalidEarlyAccess('__emscripten_stack_alloc'); +var _emscripten_stack_get_current = makeInvalidEarlyAccess('_emscripten_stack_get_current'); +var memory = makeInvalidEarlyAccess('memory'); +var __indirect_function_table = makeInvalidEarlyAccess('__indirect_function_table'); +var wasmMemory = makeInvalidEarlyAccess('wasmMemory'); + +function assignWasmExports(wasmExports) { + assert(typeof wasmExports['strerror'] != 'undefined', 'missing Wasm export: strerror'); + assert(typeof wasmExports['__main_argc_argv'] != 'undefined', 'missing Wasm export: __main_argc_argv'); + assert(typeof wasmExports['fflush'] != 'undefined', 'missing Wasm export: fflush'); + assert(typeof wasmExports['emscripten_stack_get_end'] != 'undefined', 'missing Wasm export: emscripten_stack_get_end'); + assert(typeof wasmExports['emscripten_stack_get_base'] != 'undefined', 'missing Wasm export: emscripten_stack_get_base'); + assert(typeof wasmExports['emscripten_stack_init'] != 'undefined', 'missing Wasm export: emscripten_stack_init'); + assert(typeof wasmExports['emscripten_stack_get_free'] != 'undefined', 'missing Wasm export: emscripten_stack_get_free'); + assert(typeof wasmExports['_emscripten_stack_restore'] != 'undefined', 'missing Wasm export: _emscripten_stack_restore'); + assert(typeof wasmExports['_emscripten_stack_alloc'] != 'undefined', 'missing Wasm export: _emscripten_stack_alloc'); + assert(typeof wasmExports['emscripten_stack_get_current'] != 'undefined', 'missing Wasm export: emscripten_stack_get_current'); + assert(typeof wasmExports['memory'] != 'undefined', 'missing Wasm export: memory'); + assert(typeof wasmExports['__indirect_function_table'] != 'undefined', 'missing Wasm export: __indirect_function_table'); + _strerror = createExportWrapper('strerror', 1); + _main = Module['_main'] = createExportWrapper('__main_argc_argv', 2); + _fflush = createExportWrapper('fflush', 1); + _emscripten_stack_get_end = wasmExports['emscripten_stack_get_end']; + _emscripten_stack_get_base = wasmExports['emscripten_stack_get_base']; + _emscripten_stack_init = wasmExports['emscripten_stack_init']; + _emscripten_stack_get_free = wasmExports['emscripten_stack_get_free']; + __emscripten_stack_restore = wasmExports['_emscripten_stack_restore']; + __emscripten_stack_alloc = wasmExports['_emscripten_stack_alloc']; + _emscripten_stack_get_current = wasmExports['emscripten_stack_get_current']; + memory = wasmMemory = wasmExports['memory']; + __indirect_function_table = wasmExports['__indirect_function_table']; +} -dependenciesFulfilled = function runCaller() { - // If run has never been called, and we should call run (INVOKE_RUN is true, and Module.noInitialRun is not false) - if (!calledRun) run(); - if (!calledRun) dependenciesFulfilled = runCaller; // try this again later, after new deps are fulfilled +var wasmImports = { + /** @export */ + __syscall_faccessat: ___syscall_faccessat, + /** @export */ + __syscall_fcntl64: ___syscall_fcntl64, + /** @export */ + __syscall_fstat64: ___syscall_fstat64, + /** @export */ + __syscall_getcwd: ___syscall_getcwd, + /** @export */ + __syscall_ioctl: ___syscall_ioctl, + /** @export */ + __syscall_lstat64: ___syscall_lstat64, + /** @export */ + __syscall_newfstatat: ___syscall_newfstatat, + /** @export */ + __syscall_openat: ___syscall_openat, + /** @export */ + __syscall_readlinkat: ___syscall_readlinkat, + /** @export */ + __syscall_rmdir: ___syscall_rmdir, + /** @export */ + __syscall_stat64: ___syscall_stat64, + /** @export */ + __syscall_unlinkat: ___syscall_unlinkat, + /** @export */ + _abort_js: __abort_js, + /** @export */ + emscripten_date_now: _emscripten_date_now, + /** @export */ + emscripten_resize_heap: _emscripten_resize_heap, + /** @export */ + environ_get: _environ_get, + /** @export */ + environ_sizes_get: _environ_sizes_get, + /** @export */ + exit: _exit, + /** @export */ + fd_close: _fd_close, + /** @export */ + fd_fdstat_get: _fd_fdstat_get, + /** @export */ + fd_read: _fd_read, + /** @export */ + fd_seek: _fd_seek, + /** @export */ + fd_write: _fd_write }; + +// include: postamble.js +// === Auto-generated postamble setup entry stuff === + +var calledRun; + function callMain(args = []) { assert(runDependencies == 0, 'cannot call main when async dependencies remain! (listen on Module["onRuntimeInitialized"])'); - assert(__ATPRERUN__.length == 0, 'cannot call main when preRun functions remain to be called'); + assert(typeof onPreRuns === 'undefined' || onPreRuns.length == 0, 'cannot call main when preRun functions remain to be called'); var entryFunction = _main; @@ -5434,10 +5349,10 @@ function callMain(args = []) { var argc = args.length; var argv = stackAlloc((argc + 1) * 4); var argv_ptr = argv; - args.forEach((arg) => { + for (var arg of args) { HEAPU32[((argv_ptr)>>2)] = stringToUTF8OnStack(arg); argv_ptr += 4; - }); + } HEAPU32[((argv_ptr)>>2)] = 0; try { @@ -5447,8 +5362,7 @@ function callMain(args = []) { // if we're not running an evented main loop, it's time to exit exitJS(ret, /* implicit = */ true); return ret; - } - catch (e) { + } catch (e) { return handleException(e); } } @@ -5465,22 +5379,24 @@ function stackCheckInit() { function run(args = arguments_) { if (runDependencies > 0) { + dependenciesFulfilled = run; return; } - stackCheckInit(); + stackCheckInit(); preRun(); // a preRun added a dependency, run will be called later if (runDependencies > 0) { + dependenciesFulfilled = run; return; } function doRun() { // run may have just been called through dependencies being fulfilled just in this very frame, // or while the async setStatus time below was happening - if (calledRun) return; + assert(!calledRun); calledRun = true; Module['calledRun'] = true; @@ -5490,19 +5406,19 @@ function run(args = arguments_) { preMain(); - if (Module['onRuntimeInitialized']) Module['onRuntimeInitialized'](); + Module['onRuntimeInitialized']?.(); + consumedModuleProp('onRuntimeInitialized'); - if (shouldRunNow) callMain(args); + var noInitialRun = Module['noInitialRun'] || false; + if (!noInitialRun) callMain(args); postRun(); } if (Module['setStatus']) { Module['setStatus']('Running...'); - setTimeout(function() { - setTimeout(function() { - Module['setStatus'](''); - }, 1); + setTimeout(() => { + setTimeout(() => Module['setStatus'](''), 1); doRun(); }, 1); } else @@ -5533,16 +5449,16 @@ function checkUnflushedContent() { try { // it doesn't matter if it fails _fflush(0); // also flush in the JS FS layer - ['stdout', 'stderr'].forEach(function(name) { + for (var name of ['stdout', 'stderr']) { var info = FS.analyzePath('/dev/' + name); if (!info) return; var stream = info.object; var rdev = stream.rdev; var tty = TTY.ttys[rdev]; - if (tty && tty.output && tty.output.length) { + if (tty?.output?.length) { has = true; } - }); + } } catch(e) {} out = oldOut; err = oldErr; @@ -5551,19 +5467,13 @@ function checkUnflushedContent() { } } -if (Module['preInit']) { - if (typeof Module['preInit'] == 'function') Module['preInit'] = [Module['preInit']]; - while (Module['preInit'].length > 0) { - Module['preInit'].pop()(); - } -} - -// shouldRunNow refers to calling main(), not run(). -var shouldRunNow = true; +var wasmExports; -if (Module['noInitialRun']) shouldRunNow = false; +// With async instantation wasmExports is assigned asynchronously when the +// instance is received. +createWasm(); run(); - // end include: postamble.js + diff --git a/tools/assemble/ca65.wasm b/tools/assemble/ca65.wasm index 7acbc9d5..652732bf 100755 Binary files a/tools/assemble/ca65.wasm and b/tools/assemble/ca65.wasm differ diff --git a/tools/assemble/ld65.js b/tools/assemble/ld65.js index 430b45d0..e2143519 100644 --- a/tools/assemble/ld65.js +++ b/tools/assemble/ld65.js @@ -1,8 +1,52 @@ // include: shell.js +// include: minimum_runtime_check.js +(function() { + // "30.0.0" -> 300000 + function humanReadableVersionToPacked(str) { + str = str.split('-')[0]; // Remove any trailing part from e.g. "12.53.3-alpha" + var vers = str.split('.').slice(0, 3); + while(vers.length < 3) vers.push('00'); + vers = vers.map((n, i, arr) => n.padStart(2, '0')); + return vers.join(''); + } + // 300000 -> "30.0.0" + var packedVersionToHumanReadable = n => [n / 10000 | 0, (n / 100 | 0) % 100, n % 100].join('.'); + + var TARGET_NOT_SUPPORTED = 2147483647; + + // Note: We use a typeof check here instead of optional chaining using + // globalThis because older browsers might not have globalThis defined. + var currentNodeVersion = typeof process !== 'undefined' && process.versions?.node ? humanReadableVersionToPacked(process.versions.node) : TARGET_NOT_SUPPORTED; + if (currentNodeVersion < 180300) { + throw new Error(`This emscripten-generated code requires node v${ packedVersionToHumanReadable(180300) } (detected v${packedVersionToHumanReadable(currentNodeVersion)})`); + } + + var userAgent = typeof navigator !== 'undefined' && navigator.userAgent; + if (!userAgent) { + return; + } + + var currentSafariVersion = userAgent.includes("Safari/") && !userAgent.includes("Chrome/") && userAgent.match(/Version\/(\d+\.?\d*\.?\d*)/) ? humanReadableVersionToPacked(userAgent.match(/Version\/(\d+\.?\d*\.?\d*)/)[1]) : TARGET_NOT_SUPPORTED; + if (currentSafariVersion < 150000) { + throw new Error(`This emscripten-generated code requires Safari v${ packedVersionToHumanReadable(150000) } (detected v${currentSafariVersion})`); + } + + var currentFirefoxVersion = userAgent.match(/Firefox\/(\d+(?:\.\d+)?)/) ? parseFloat(userAgent.match(/Firefox\/(\d+(?:\.\d+)?)/)[1]) : TARGET_NOT_SUPPORTED; + if (currentFirefoxVersion < 79) { + throw new Error(`This emscripten-generated code requires Firefox v79 (detected v${currentFirefoxVersion})`); + } + + var currentChromeVersion = userAgent.match(/Chrome\/(\d+(?:\.\d+)?)/) ? parseFloat(userAgent.match(/Chrome\/(\d+(?:\.\d+)?)/)[1]) : TARGET_NOT_SUPPORTED; + if (currentChromeVersion < 85) { + throw new Error(`This emscripten-generated code requires Chrome v85 (detected v${currentChromeVersion})`); + } +})(); + +// end include: minimum_runtime_check.js // The Module object: Our interface to the outside world. We import // and export values on it. There are various ways Module can be used: // 1. Not defined. We create it here -// 2. A function parameter, function(Module) { ..generated code.. } +// 2. A function parameter, function(moduleArg) => Promise // 3. pre-run appended it, var Module = {}; ..generated code.. // 4. External script tag defines var Module. // We need to check if Module already exists (e.g. case 3 above). @@ -14,36 +58,36 @@ // can continue to use Module afterwards as well. var Module = typeof Module != 'undefined' ? Module : {}; +// Determine the runtime environment we are in. You can customize this by +// setting the ENVIRONMENT setting at compile time (see settings.js). + +// Attempt to auto-detect the environment +var ENVIRONMENT_IS_WEB = !!globalThis.window; +var ENVIRONMENT_IS_WORKER = !!globalThis.WorkerGlobalScope; +// N.b. Electron.js environment is simultaneously a NODE-environment, but +// also a web environment. +var ENVIRONMENT_IS_NODE = globalThis.process?.versions?.node && globalThis.process?.type != 'renderer'; +var ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER; + // --pre-jses are emitted after the Module integration code, so that they can // refer to Module (if they choose; they can also define Module) -// Sometimes an existing Module object exists with properties -// meant to overwrite the default module functionality. Here -// we collect those properties and reapply _after_ we configure -// the current environment's defaults to avoid having to be so -// defensive during initialization. -var moduleOverrides = Object.assign({}, Module); - var arguments_ = []; var thisProgram = './this.program'; var quit_ = (status, toThrow) => { throw toThrow; }; -// Determine the runtime environment we are in. You can customize this by -// setting the ENVIRONMENT setting at compile time (see settings.js). - -// Attempt to auto-detect the environment -var ENVIRONMENT_IS_WEB = typeof window == 'object'; -var ENVIRONMENT_IS_WORKER = typeof importScripts == 'function'; -// N.b. Electron.js environment is simultaneously a NODE-environment, but -// also a web environment. -var ENVIRONMENT_IS_NODE = typeof process == 'object' && typeof process.versions == 'object' && typeof process.versions.node == 'string'; -var ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER; +// In MODULARIZE mode _scriptName needs to be captured already at the very top of the page immediately when the page is parsed, so it is generated there +// before the page load. In non-MODULARIZE modes generate it here. +var _scriptName = globalThis.document?.currentScript?.src; -if (Module['ENVIRONMENT']) { - throw new Error('Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -sENVIRONMENT=web or -sENVIRONMENT=node)'); +if (typeof __filename != 'undefined') { // Node + _scriptName = __filename; +} else +if (ENVIRONMENT_IS_WORKER) { + _scriptName = self.location.href; } // `/` should be present at the end if `scriptDirectory` is not empty @@ -56,192 +100,72 @@ function locateFile(path) { } // Hooks that are implemented differently in different runtime environments. -var read_, - readAsync, - readBinary; +var readAsync, readBinary; if (ENVIRONMENT_IS_NODE) { - if (typeof process == 'undefined' || !process.release || process.release.name !== 'node') throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); - - var nodeVersion = process.versions.node; - var numericVersion = nodeVersion.split('.').slice(0, 3); - numericVersion = (numericVersion[0] * 10000) + (numericVersion[1] * 100) + (numericVersion[2].split('-')[0] * 1); - var minVersion = 160000; - if (numericVersion < 160000) { - throw new Error('This emscripten-generated code requires node v16.0.0 (detected v' + nodeVersion + ')'); - } + const isNode = globalThis.process?.versions?.node && globalThis.process?.type != 'renderer'; + if (!isNode) throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); - // `require()` is no-op in an ESM module, use `createRequire()` to construct - // the require()` function. This is only necessary for multi-environment - // builds, `-sENVIRONMENT=node` emits a static import declaration instead. - // TODO: Swap all `require()`'s with `import()`'s? // These modules will usually be used on Node.js. Load them eagerly to avoid // the complexity of lazy-loading. - var fs = require('fs'); - var nodePath = require('path'); + var fs = require('node:fs'); - if (ENVIRONMENT_IS_WORKER) { - scriptDirectory = nodePath.dirname(scriptDirectory) + '/'; - } else { - scriptDirectory = __dirname + '/'; - } + scriptDirectory = __dirname + '/'; // include: node_shell_read.js -read_ = (filename, binary) => { - // We need to re-wrap `file://` strings to URLs. Normalizing isn't - // necessary in that case, the path should already be absolute. - filename = isFileURI(filename) ? new URL(filename) : nodePath.normalize(filename); - return fs.readFileSync(filename, binary ? undefined : 'utf8'); -}; - readBinary = (filename) => { - var ret = read_(filename, true); - if (!ret.buffer) { - ret = new Uint8Array(ret); - } - assert(ret.buffer); + // We need to re-wrap `file://` strings to URLs. + filename = isFileURI(filename) ? new URL(filename) : filename; + var ret = fs.readFileSync(filename); + assert(Buffer.isBuffer(ret)); return ret; }; -readAsync = (filename, onload, onerror, binary = true) => { - // See the comment in the `read_` function. - filename = isFileURI(filename) ? new URL(filename) : nodePath.normalize(filename); - fs.readFile(filename, binary ? undefined : 'utf8', (err, data) => { - if (err) onerror(err); - else onload(binary ? data.buffer : data); - }); +readAsync = async (filename, binary = true) => { + // See the comment in the `readBinary` function. + filename = isFileURI(filename) ? new URL(filename) : filename; + var ret = fs.readFileSync(filename, binary ? undefined : 'utf8'); + assert(binary ? Buffer.isBuffer(ret) : typeof ret == 'string'); + return ret; }; // end include: node_shell_read.js - if (!Module['thisProgram'] && process.argv.length > 1) { + if (process.argv.length > 1) { thisProgram = process.argv[1].replace(/\\/g, '/'); } arguments_ = process.argv.slice(2); + // MODULARIZE will export the module in the proper place outside, we don't need to export here if (typeof module != 'undefined') { module['exports'] = Module; } - process.on('uncaughtException', (ex) => { - // suppress ExitStatus exceptions from showing an error - if (ex !== 'unwind' && !(ex instanceof ExitStatus) && !(ex.context instanceof ExitStatus)) { - throw ex; - } - }); - quit_ = (status, toThrow) => { process.exitCode = status; throw toThrow; }; - Module['inspect'] = () => '[Emscripten Module object]'; - } else if (ENVIRONMENT_IS_SHELL) { - if ((typeof process == 'object' && typeof require === 'function') || typeof window == 'object' || typeof importScripts == 'function') throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); - - if (typeof read != 'undefined') { - read_ = read; - } - - readBinary = (f) => { - if (typeof readbuffer == 'function') { - return new Uint8Array(readbuffer(f)); - } - let data = read(f, 'binary'); - assert(typeof data == 'object'); - return data; - }; - - readAsync = (f, onload, onerror) => { - setTimeout(() => onload(readBinary(f))); - }; - - if (typeof clearTimeout == 'undefined') { - globalThis.clearTimeout = (id) => {}; - } - - if (typeof setTimeout == 'undefined') { - // spidermonkey lacks setTimeout but we use it above in readAsync. - globalThis.setTimeout = (f) => (typeof f == 'function') ? f() : abort(); - } - - if (typeof scriptArgs != 'undefined') { - arguments_ = scriptArgs; - } else if (typeof arguments != 'undefined') { - arguments_ = arguments; - } - - if (typeof quit == 'function') { - quit_ = (status, toThrow) => { - // Unlike node which has process.exitCode, d8 has no such mechanism. So we - // have no way to set the exit code and then let the program exit with - // that code when it naturally stops running (say, when all setTimeouts - // have completed). For that reason, we must call `quit` - the only way to - // set the exit code - but quit also halts immediately. To increase - // consistency with node (and the web) we schedule the actual quit call - // using a setTimeout to give the current stack and any exception handlers - // a chance to run. This enables features such as addOnPostRun (which - // expected to be able to run code after main returns). - setTimeout(() => { - if (!(toThrow instanceof ExitStatus)) { - let toLog = toThrow; - if (toThrow && typeof toThrow == 'object' && toThrow.stack) { - toLog = [toThrow, toThrow.stack]; - } - err(`exiting due to exception: ${toLog}`); - } - quit(status); - }); - throw toThrow; - }; - } - - if (typeof print != 'undefined') { - // Prefer to use print/printErr where they exist, as they usually work better. - if (typeof console == 'undefined') console = /** @type{!Console} */({}); - console.log = /** @type{!function(this:Console, ...*): undefined} */ (print); - console.warn = console.error = /** @type{!function(this:Console, ...*): undefined} */ (typeof printErr != 'undefined' ? printErr : print); - } - } else // Note that this includes Node.js workers when relevant (pthreads is enabled). // Node.js workers are detected as a combination of ENVIRONMENT_IS_WORKER and // ENVIRONMENT_IS_NODE. if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { - if (ENVIRONMENT_IS_WORKER) { // Check worker, not web, since window could be polyfilled - scriptDirectory = self.location.href; - } else if (typeof document != 'undefined' && document.currentScript) { // web - scriptDirectory = document.currentScript.src; - } - // blob urls look like blob:http://site.com/etc/etc and we cannot infer anything from them. - // otherwise, slice off the final part of the url to find the script directory. - // if scriptDirectory does not contain a slash, lastIndexOf will return -1, - // and scriptDirectory will correctly be replaced with an empty string. - // If scriptDirectory contains a query (starting with ?) or a fragment (starting with #), - // they are removed because they could contain a slash. - if (scriptDirectory.indexOf('blob:') !== 0) { - scriptDirectory = scriptDirectory.substr(0, scriptDirectory.replace(/[?#].*/, "").lastIndexOf('/')+1); - } else { - scriptDirectory = ''; + try { + scriptDirectory = new URL('.', _scriptName).href; // includes trailing slash + } catch { + // Must be a `blob:` or `data:` URL (e.g. `blob:http://site.com/etc/etc`), we cannot + // infer anything from them. } - if (!(typeof window == 'object' || typeof importScripts == 'function')) throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); + if (!(globalThis.window || globalThis.WorkerGlobalScope)) throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); - // Differentiate the Web Worker from the Node Worker case, as reading must - // be done differently. { // include: web_or_worker_shell_read.js -read_ = (url) => { - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, false); - xhr.send(null); - return xhr.responseText; - } - - if (ENVIRONMENT_IS_WORKER) { +if (ENVIRONMENT_IS_WORKER) { readBinary = (url) => { var xhr = new XMLHttpRequest(); xhr.open('GET', url, false); @@ -251,21 +175,33 @@ read_ = (url) => { }; } - readAsync = (url, onload, onerror) => { - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, true); - xhr.responseType = 'arraybuffer'; - xhr.onload = () => { - if (xhr.status == 200 || (xhr.status == 0 && xhr.response)) { // file URLs can return 0 - onload(xhr.response); - return; - } - onerror(); - }; - xhr.onerror = onerror; - xhr.send(null); - } - + readAsync = async (url) => { + // Fetch has some additional restrictions over XHR, like it can't be used on a file:// url. + // See https://github.com/github/fetch/pull/92#issuecomment-140665932 + // Cordova or Electron apps are typically loaded from a file:// url. + // So use XHR on webview if URL is a file URL. + if (isFileURI(url)) { + return new Promise((resolve, reject) => { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = 'arraybuffer'; + xhr.onload = () => { + if (xhr.status == 200 || (xhr.status == 0 && xhr.response)) { // file URLs can return 0 + resolve(xhr.response); + return; + } + reject(xhr.status); + }; + xhr.onerror = reject; + xhr.send(null); + }); + } + var response = await fetch(url, { credentials: 'same-origin' }); + if (response.ok) { + return response.arrayBuffer(); + } + throw new Error(response.status + ' : ' + response.url); + }; // end include: web_or_worker_shell_read.js } } else @@ -273,43 +209,9 @@ read_ = (url) => { throw new Error('environment detection error'); } -var out = Module['print'] || console.log.bind(console); -var err = Module['printErr'] || console.error.bind(console); - -// Merge back in the overrides -Object.assign(Module, moduleOverrides); -// Free the object hierarchy contained in the overrides, this lets the GC -// reclaim data used e.g. in memoryInitializerRequest, which is a large typed array. -moduleOverrides = null; -checkIncomingModuleAPI(); - -// Emit code to handle expected values on the Module object. This applies Module.x -// to the proper local x. This has two benefits: first, we only emit it if it is -// expected to arrive, and second, by using a local everywhere else that can be -// minified. - -if (Module['arguments']) arguments_ = Module['arguments'];legacyModuleProp('arguments', 'arguments_'); - -if (Module['thisProgram']) thisProgram = Module['thisProgram'];legacyModuleProp('thisProgram', 'thisProgram'); - -if (Module['quit']) quit_ = Module['quit'];legacyModuleProp('quit', 'quit_'); - -// perform assertions in shell.js after we set up out() and err(), as otherwise if an assertion fails it cannot print the message -// Assertions on removed incoming Module JS APIs. -assert(typeof Module['memoryInitializerPrefixURL'] == 'undefined', 'Module.memoryInitializerPrefixURL option was removed, use Module.locateFile instead'); -assert(typeof Module['pthreadMainPrefixURL'] == 'undefined', 'Module.pthreadMainPrefixURL option was removed, use Module.locateFile instead'); -assert(typeof Module['cdInitializerPrefixURL'] == 'undefined', 'Module.cdInitializerPrefixURL option was removed, use Module.locateFile instead'); -assert(typeof Module['filePackagePrefixURL'] == 'undefined', 'Module.filePackagePrefixURL option was removed, use Module.locateFile instead'); -assert(typeof Module['read'] == 'undefined', 'Module.read option was removed (modify read_ in JS)'); -assert(typeof Module['readAsync'] == 'undefined', 'Module.readAsync option was removed (modify readAsync in JS)'); -assert(typeof Module['readBinary'] == 'undefined', 'Module.readBinary option was removed (modify readBinary in JS)'); -assert(typeof Module['setWindowTitle'] == 'undefined', 'Module.setWindowTitle option was removed (modify emscripten_set_window_title in JS)'); -assert(typeof Module['TOTAL_MEMORY'] == 'undefined', 'Module.TOTAL_MEMORY has been renamed Module.INITIAL_MEMORY'); -legacyModuleProp('asm', 'wasmExports'); -legacyModuleProp('read', 'read_'); -legacyModuleProp('readAsync', 'readAsync'); -legacyModuleProp('readBinary', 'readBinary'); -legacyModuleProp('setWindowTitle', 'setWindowTitle'); +var out = console.log.bind(console); +var err = console.error.bind(console); + var IDBFS = 'IDBFS is no longer included by default; build with -lidbfs.js'; var PROXYFS = 'PROXYFS is no longer included by default; build with -lproxyfs.js'; var WORKERFS = 'WORKERFS is no longer included by default; build with -lworkerfs.js'; @@ -320,10 +222,13 @@ var OPFS = 'OPFS is no longer included by default; build with -lopfs.js'; -assert(!ENVIRONMENT_IS_SHELL, "shell environment detected but not enabled at build time. Add 'shell' to `-sENVIRONMENT` to enable."); +// perform assertions in shell.js after we set up out() and err(), as otherwise +// if an assertion fails it cannot print the message +assert(!ENVIRONMENT_IS_SHELL, 'shell environment detected but not enabled at build time (add `shell` to `-sENVIRONMENT` to enable)'); // end include: shell.js + // include: preamble.js // === Preamble library stuff === @@ -335,43 +240,14 @@ assert(!ENVIRONMENT_IS_SHELL, "shell environment detected but not enabled at bui // An online HTML version (which may be of a different version of Emscripten) // is up at http://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html -var wasmBinary; -if (Module['wasmBinary']) wasmBinary = Module['wasmBinary'];legacyModuleProp('wasmBinary', 'wasmBinary'); - -if (typeof WebAssembly != 'object') { - abort('no native wasm support detected'); -} - -// include: base64Utils.js -// Converts a string of base64 into a byte array (Uint8Array). -function intArrayFromBase64(s) { - if (typeof ENVIRONMENT_IS_NODE != 'undefined' && ENVIRONMENT_IS_NODE) { - var buf = Buffer.from(s, 'base64'); - return new Uint8Array(buf.buffer, buf.byteOffset, buf.length); - } +var wasmBinary; - var decoded = atob(s); - var bytes = new Uint8Array(decoded.length); - for (var i = 0 ; i < decoded.length ; ++i) { - bytes[i] = decoded.charCodeAt(i); - } - return bytes; +if (!globalThis.WebAssembly) { + err('no native wasm support detected'); } -// If filename is a base64 data URI, parses and returns data (Buffer on node, -// Uint8Array otherwise). If filename is not a base64 data URI, returns undefined. -function tryParseAsDataURI(filename) { - if (!isDataURI(filename)) { - return; - } - - return intArrayFromBase64(filename.slice(dataURIPrefix.length)); -} -// end include: base64Utils.js // Wasm globals -var wasmMemory; - //======================================== // Runtime essentials //======================================== @@ -381,7 +257,7 @@ var wasmMemory; var ABORT = false; // set by exit() and abort(). Passed to 'onExit' handler. -// NOTE: This is also used as the process return code code in shell environments +// NOTE: This is also used as the process return code in shell environments // but only when noExitRuntime is false. var EXITSTATUS; @@ -398,48 +274,21 @@ function assert(condition, text) { // We used to include malloc/free by default in the past. Show a helpful error in // builds with assertions. - -// Memory management - -var HEAP, -/** @type {!Int8Array} */ - HEAP8, -/** @type {!Uint8Array} */ - HEAPU8, -/** @type {!Int16Array} */ - HEAP16, -/** @type {!Uint16Array} */ - HEAPU16, -/** @type {!Int32Array} */ - HEAP32, -/** @type {!Uint32Array} */ - HEAPU32, -/** @type {!Float32Array} */ - HEAPF32, -/** @type {!Float64Array} */ - HEAPF64; - -function updateMemoryViews() { - var b = wasmMemory.buffer; - Module['HEAP8'] = HEAP8 = new Int8Array(b); - Module['HEAP16'] = HEAP16 = new Int16Array(b); - Module['HEAPU8'] = HEAPU8 = new Uint8Array(b); - Module['HEAPU16'] = HEAPU16 = new Uint16Array(b); - Module['HEAP32'] = HEAP32 = new Int32Array(b); - Module['HEAPU32'] = HEAPU32 = new Uint32Array(b); - Module['HEAPF32'] = HEAPF32 = new Float32Array(b); - Module['HEAPF64'] = HEAPF64 = new Float64Array(b); +function _malloc() { + abort('malloc() called but not included in the build - add `_malloc` to EXPORTED_FUNCTIONS'); +} +function _free() { + // Show a helpful error since we used to include free by default in the past. + abort('free() called but not included in the build - add `_free` to EXPORTED_FUNCTIONS'); } -assert(!Module['STACK_SIZE'], 'STACK_SIZE can no longer be set at runtime. Use -sSTACK_SIZE at link time') - -assert(typeof Int32Array != 'undefined' && typeof Float64Array !== 'undefined' && Int32Array.prototype.subarray != undefined && Int32Array.prototype.set != undefined, - 'JS engine does not provide full typed array support'); - -// If memory is defined in wasm, the user can't provide it, or set INITIAL_MEMORY -assert(!Module['wasmMemory'], 'Use of `wasmMemory` detected. Use -sIMPORTED_MEMORY to define wasmMemory externally'); -assert(!Module['INITIAL_MEMORY'], 'Detected runtime INITIAL_MEMORY setting. Use -sIMPORTED_MEMORY to define wasmMemory dynamically'); +/** + * Indicates whether filename is delivered via file protocol (as opposed to http/https) + * @noinline + */ +var isFileURI = (filename) => filename.startsWith('file://'); +// include: runtime_common.js // include: runtime_stack_check.js // Initializes the stack cookie. Called at the startup of main and at the startup of each thread in pthreads mode. function writeStackCookie() { @@ -478,24 +327,164 @@ function checkStackCookie() { } } // end include: runtime_stack_check.js -// include: runtime_assertions.js +// include: runtime_exceptions.js +// Base Emscripten EH error class +class EmscriptenEH {} + +class EmscriptenSjLj extends EmscriptenEH {} + +// end include: runtime_exceptions.js +// include: runtime_debug.js +var runtimeDebug = true; // Switch to false at runtime to disable logging at the right times + +// Used by XXXXX_DEBUG settings to output debug messages. +function dbg(...args) { + if (!runtimeDebug && typeof runtimeDebug != 'undefined') return; + // TODO(sbc): Make this configurable somehow. Its not always convenient for + // logging to show up as warnings. + console.warn(...args); +} + // Endianness check -(function() { +(() => { var h16 = new Int16Array(1); var h8 = new Int8Array(h16.buffer); h16[0] = 0x6373; - if (h8[0] !== 0x73 || h8[1] !== 0x63) throw 'Runtime error: expected the system to be little-endian! (Run with -sSUPPORT_BIG_ENDIAN to bypass)'; + if (h8[0] !== 0x73 || h8[1] !== 0x63) abort('Runtime error: expected the system to be little-endian! (Run with -sSUPPORT_BIG_ENDIAN to bypass)'); })(); -// end include: runtime_assertions.js -var __ATPRERUN__ = []; // functions called before the runtime is initialized -var __ATINIT__ = []; // functions called during startup -var __ATMAIN__ = []; // functions called when main() is to be run -var __ATEXIT__ = []; // functions called during shutdown -var __ATPOSTRUN__ = []; // functions called after the main() is called +function consumedModuleProp(prop) { + if (!Object.getOwnPropertyDescriptor(Module, prop)) { + Object.defineProperty(Module, prop, { + configurable: true, + set() { + abort(`Attempt to set \`Module.${prop}\` after it has already been processed. This can happen, for example, when code is injected via '--post-js' rather than '--pre-js'`); + + } + }); + } +} + +function makeInvalidEarlyAccess(name) { + return () => assert(false, `call to '${name}' via reference taken before Wasm module initialization`); + +} + +function ignoredModuleProp(prop) { + if (Object.getOwnPropertyDescriptor(Module, prop)) { + abort(`\`Module.${prop}\` was supplied but \`${prop}\` not included in INCOMING_MODULE_JS_API`); + } +} + +// forcing the filesystem exports a few things by default +function isExportedByForceFilesystem(name) { + return name === 'FS_createPath' || + name === 'FS_createDataFile' || + name === 'FS_createPreloadedFile' || + name === 'FS_preloadFile' || + name === 'FS_unlink' || + name === 'addRunDependency' || + // The old FS has some functionality that WasmFS lacks. + name === 'FS_createLazyFile' || + name === 'FS_createDevice' || + name === 'removeRunDependency'; +} + +/** + * Intercept access to a symbols in the global symbol. This enables us to give + * informative warnings/errors when folks attempt to use symbols they did not + * include in their build, or no symbols that no longer exist. + * + * We don't define this in MODULARIZE mode since in that mode emscripten symbols + * are never placed in the global scope. + */ +function hookGlobalSymbolAccess(sym, func) { + if (!Object.getOwnPropertyDescriptor(globalThis, sym)) { + Object.defineProperty(globalThis, sym, { + configurable: true, + get() { + func(); + return undefined; + } + }); + } +} + +function missingGlobal(sym, msg) { + hookGlobalSymbolAccess(sym, () => { + warnOnce(`\`${sym}\` is no longer defined by emscripten. ${msg}`); + }); +} + +missingGlobal('buffer', 'Please use HEAP8.buffer or wasmMemory.buffer'); +missingGlobal('asm', 'Please use wasmExports instead'); + +function missingLibrarySymbol(sym) { + hookGlobalSymbolAccess(sym, () => { + // Can't `abort()` here because it would break code that does runtime + // checks. e.g. `if (typeof SDL === 'undefined')`. + var msg = `\`${sym}\` is a library symbol and not included by default; add it to your library.js __deps or to DEFAULT_LIBRARY_FUNCS_TO_INCLUDE on the command line`; + // DEFAULT_LIBRARY_FUNCS_TO_INCLUDE requires the name as it appears in + // library.js, which means $name for a JS name with no prefix, or name + // for a JS name like _name. + var librarySymbol = sym; + if (!librarySymbol.startsWith('_')) { + librarySymbol = '$' + sym; + } + msg += ` (e.g. -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='${librarySymbol}')`; + if (isExportedByForceFilesystem(sym)) { + msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you'; + } + warnOnce(msg); + }); + + // Any symbol that is not included from the JS library is also (by definition) + // not exported on the Module object. + unexportedRuntimeSymbol(sym); +} + +function unexportedRuntimeSymbol(sym) { + if (!Object.getOwnPropertyDescriptor(Module, sym)) { + Object.defineProperty(Module, sym, { + configurable: true, + get() { + var msg = `'${sym}' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the Emscripten FAQ)`; + if (isExportedByForceFilesystem(sym)) { + msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you'; + } + abort(msg); + }, + }); + } +} + +// end include: runtime_debug.js +// Memory management var runtimeInitialized = false; + + +function updateMemoryViews() { + var b = wasmMemory.buffer; + HEAP8 = new Int8Array(b); + HEAP16 = new Int16Array(b); + HEAPU8 = new Uint8Array(b); + HEAPU16 = new Uint16Array(b); + HEAP32 = new Int32Array(b); + HEAPU32 = new Uint32Array(b); + HEAPF32 = new Float32Array(b); + HEAPF64 = new Float64Array(b); + HEAP64 = new BigInt64Array(b); + HEAPU64 = new BigUint64Array(b); +} + +// include: memoryprofiler.js +// end include: memoryprofiler.js +// end include: runtime_common.js +assert(globalThis.Int32Array && globalThis.Float64Array && Int32Array.prototype.subarray && Int32Array.prototype.set, + 'JS engine does not provide full typed array support'); + function preRun() { if (Module['preRun']) { if (typeof Module['preRun'] == 'function') Module['preRun'] = [Module['preRun']]; @@ -503,7 +492,10 @@ function preRun() { addOnPreRun(Module['preRun'].shift()); } } - callRuntimeCallbacks(__ATPRERUN__); + consumedModuleProp('preRun'); + // Begin ATPRERUNS hooks + callRuntimeCallbacks(onPreRuns); + // End ATPRERUNS hooks } function initRuntime() { @@ -512,23 +504,26 @@ function initRuntime() { checkStackCookie(); - -if (!Module["noFSInit"] && !FS.init.initialized) - FS.init(); -FS.ignorePermissions = false; - + // Begin ATINITS hooks + if (!Module['noFSInit'] && !FS.initialized) FS.init(); TTY.init(); - callRuntimeCallbacks(__ATINIT__); + // End ATINITS hooks + + wasmExports['__wasm_call_ctors'](); + + // Begin ATPOSTCTORS hooks + FS.ignorePermissions = false; + // End ATPOSTCTORS hooks } function preMain() { checkStackCookie(); - - callRuntimeCallbacks(__ATMAIN__); + // No ATMAINS hooks } function postRun() { checkStackCookie(); + // PThreads reuse the runtime from the main thread. if (Module['postRun']) { if (typeof Module['postRun'] == 'function') Module['postRun'] = [Module['postRun']]; @@ -536,138 +531,25 @@ function postRun() { addOnPostRun(Module['postRun'].shift()); } } + consumedModuleProp('postRun'); - callRuntimeCallbacks(__ATPOSTRUN__); -} - -function addOnPreRun(cb) { - __ATPRERUN__.unshift(cb); -} - -function addOnInit(cb) { - __ATINIT__.unshift(cb); -} - -function addOnPreMain(cb) { - __ATMAIN__.unshift(cb); -} - -function addOnExit(cb) { -} - -function addOnPostRun(cb) { - __ATPOSTRUN__.unshift(cb); -} - -// include: runtime_math.js -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/fround - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32 - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc - -assert(Math.imul, 'This browser does not support Math.imul(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill'); -assert(Math.fround, 'This browser does not support Math.fround(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill'); -assert(Math.clz32, 'This browser does not support Math.clz32(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill'); -assert(Math.trunc, 'This browser does not support Math.trunc(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill'); -// end include: runtime_math.js -// A counter of dependencies for calling run(). If we need to -// do asynchronous work before running, increment this and -// decrement it. Incrementing must happen in a place like -// Module.preRun (used by emcc to add file preloading). -// Note that you can add dependencies in preRun, even though -// it happens right before run - run will be postponed until -// the dependencies are met. -var runDependencies = 0; -var runDependencyWatcher = null; -var dependenciesFulfilled = null; // overridden to take different actions when all run dependencies are fulfilled -var runDependencyTracking = {}; - -function getUniqueRunDependency(id) { - var orig = id; - while (1) { - if (!runDependencyTracking[id]) return id; - id = orig + Math.random(); - } -} - -function addRunDependency(id) { - runDependencies++; - - if (Module['monitorRunDependencies']) { - Module['monitorRunDependencies'](runDependencies); - } - - if (id) { - assert(!runDependencyTracking[id]); - runDependencyTracking[id] = 1; - if (runDependencyWatcher === null && typeof setInterval != 'undefined') { - // Check for missing dependencies every few seconds - runDependencyWatcher = setInterval(() => { - if (ABORT) { - clearInterval(runDependencyWatcher); - runDependencyWatcher = null; - return; - } - var shown = false; - for (var dep in runDependencyTracking) { - if (!shown) { - shown = true; - err('still waiting on run dependencies:'); - } - err(`dependency: ${dep}`); - } - if (shown) { - err('(end of list)'); - } - }, 10000); - } - } else { - err('warning: run dependency added without ID'); - } -} - -function removeRunDependency(id) { - runDependencies--; - - if (Module['monitorRunDependencies']) { - Module['monitorRunDependencies'](runDependencies); - } - - if (id) { - assert(runDependencyTracking[id]); - delete runDependencyTracking[id]; - } else { - err('warning: run dependency removed without ID'); - } - if (runDependencies == 0) { - if (runDependencyWatcher !== null) { - clearInterval(runDependencyWatcher); - runDependencyWatcher = null; - } - if (dependenciesFulfilled) { - var callback = dependenciesFulfilled; - dependenciesFulfilled = null; - callback(); // can add another dependenciesFulfilled - } - } + // Begin ATPOSTRUNS hooks + callRuntimeCallbacks(onPostRuns); + // End ATPOSTRUNS hooks } -/** @param {string|number=} what */ +/** + * @param {string|number=} what + */ function abort(what) { - if (Module['onAbort']) { - Module['onAbort'](what); - } + Module['onAbort']?.(what); - what = 'Aborted(' + what + ')'; + what = `Aborted(${what})`; // TODO(sbc): Should we remove printing and leave it up to whoever // catches the exception? err(what); ABORT = true; - EXITSTATUS = 1; // Use a wasm runtime error, because a JS error might be seen as a foreign // exception, which means we'd run destructors on it. We need the error to @@ -679,7 +561,7 @@ function abort(what) { // allows this in the wasm spec. // Suppress closure compiler warning here. Closure compiler's builtin extern - // defintion for WebAssembly.RuntimeError claims it takes no arguments even + // definition for WebAssembly.RuntimeError claims it takes no arguments even // though it can. // TODO(https://github.com/google/closure-compiler/pull/3913): Remove if/when upstream closure gets fixed. /** @suppress {checkTypes} */ @@ -691,41 +573,23 @@ function abort(what) { throw e; } -// include: memoryprofiler.js -// end include: memoryprofiler.js -// include: URIUtils.js -// Prefix of data URIs emitted by SINGLE_FILE and related options. -var dataURIPrefix = 'data:application/octet-stream;base64,'; +function createExportWrapper(name, nargs) { + return (...args) => { + assert(runtimeInitialized, `native function \`${name}\` called before runtime initialization`); + var f = wasmExports[name]; + assert(f, `exported native function \`${name}\` not found`); + // Only assert for too many arguments. Too few can be valid since the missing arguments will be zero filled. + assert(args.length <= nargs, `native function \`${name}\` called with ${args.length} args but expects ${nargs}`); + return f(...args); + }; +} -/** - * Indicates whether filename is a base64 data URI. - * @noinline - */ -var isDataURI = (filename) => filename.startsWith(dataURIPrefix); +var wasmBinaryFile; -/** - * Indicates whether filename is delivered via file protocol (as opposed to http/https) - * @noinline - */ -var isFileURI = (filename) => filename.startsWith('file://'); -// end include: URIUtils.js -function createExportWrapper(name) { - return function() { - assert(runtimeInitialized, `native function \`${name}\` called before runtime initialization`); - var f = wasmExports[name]; - assert(f, `exported native function \`${name}\` not found`); - return f.apply(null, arguments); - }; +function findWasmBinary() { + return locateFile('ld65.wasm'); } -// include: runtime_exceptions.js -// end include: runtime_exceptions.js -var wasmBinaryFile; - wasmBinaryFile = 'ld65.wasm'; - if (!isDataURI(wasmBinaryFile)) { - wasmBinaryFile = locateFile(wasmBinaryFile); - } - function getBinarySync(file) { if (file == wasmBinaryFile && wasmBinary) { return new Uint8Array(wasmBinary); @@ -733,99 +597,82 @@ function getBinarySync(file) { if (readBinary) { return readBinary(file); } - throw "both async and sync fetching of the wasm failed"; + // Throwing a plain string here, even though it not normally advisable since + // this gets turning into an `abort` in instantiateArrayBuffer. + throw 'both async and sync fetching of the wasm failed'; } -function getBinaryPromise(binaryFile) { - // If we don't have the binary yet, try to load it asynchronously. - // Fetch has some additional restrictions over XHR, like it can't be used on a file:// url. - // See https://github.com/github/fetch/pull/92#issuecomment-140665932 - // Cordova or Electron apps are typically loaded from a file:// url. - // So use fetch if it is available and the url is not a file, otherwise fall back to XHR. - if (!wasmBinary - && (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER)) { - if (typeof fetch == 'function' - && !isFileURI(binaryFile) - ) { - return fetch(binaryFile, { credentials: 'same-origin' }).then((response) => { - if (!response['ok']) { - throw "failed to load wasm binary file at '" + binaryFile + "'"; - } - return response['arrayBuffer'](); - }).catch(() => getBinarySync(binaryFile)); - } - else if (readAsync) { - // fetch is not available or url is file => try XHR (readAsync uses XHR internally) - return new Promise((resolve, reject) => { - readAsync(binaryFile, (response) => resolve(new Uint8Array(/** @type{!ArrayBuffer} */(response))), reject) - }); +async function getWasmBinary(binaryFile) { + // If we don't have the binary yet, load it asynchronously using readAsync. + if (!wasmBinary) { + // Fetch the binary using readAsync + try { + var response = await readAsync(binaryFile); + return new Uint8Array(response); + } catch { + // Fall back to getBinarySync below; } } // Otherwise, getBinarySync should be able to get it synchronously - return Promise.resolve().then(() => getBinarySync(binaryFile)); + return getBinarySync(binaryFile); } -function instantiateArrayBuffer(binaryFile, imports, receiver) { - return getBinaryPromise(binaryFile).then((binary) => { - return WebAssembly.instantiate(binary, imports); - }).then((instance) => { +async function instantiateArrayBuffer(binaryFile, imports) { + try { + var binary = await getWasmBinary(binaryFile); + var instance = await WebAssembly.instantiate(binary, imports); return instance; - }).then(receiver, (reason) => { + } catch (reason) { err(`failed to asynchronously prepare wasm: ${reason}`); // Warn on some common problems. - if (isFileURI(wasmBinaryFile)) { - err(`warning: Loading from a file URI (${wasmBinaryFile}) is not supported in most browsers. See https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-run-a-local-webserver-for-testing-why-does-my-program-stall-in-downloading-or-preparing`); + if (isFileURI(binaryFile)) { + err(`warning: Loading from a file URI (${binaryFile}) is not supported in most browsers. See https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-run-a-local-webserver-for-testing-why-does-my-program-stall-in-downloading-or-preparing`); } abort(reason); - }); + } } -function instantiateAsync(binary, binaryFile, imports, callback) { - if (!binary && - typeof WebAssembly.instantiateStreaming == 'function' && - !isDataURI(binaryFile) && +async function instantiateAsync(binary, binaryFile, imports) { + if (!binary // Don't use streaming for file:// delivered objects in a webview, fetch them synchronously. - !isFileURI(binaryFile) && + && !isFileURI(binaryFile) // Avoid instantiateStreaming() on Node.js environment for now, as while // Node.js v18.1.0 implements it, it does not have a full fetch() // implementation yet. // // Reference: // https://github.com/emscripten-core/emscripten/pull/16917 - !ENVIRONMENT_IS_NODE && - typeof fetch == 'function') { - return fetch(binaryFile, { credentials: 'same-origin' }).then((response) => { - // Suppress closure warning here since the upstream definition for - // instantiateStreaming only allows Promise rather than - // an actual Response. - // TODO(https://github.com/google/closure-compiler/pull/3913): Remove if/when upstream closure is fixed. - /** @suppress {checkTypes} */ - var result = WebAssembly.instantiateStreaming(response, imports); - - return result.then( - callback, - function(reason) { - // We expect the most common failure cause to be a bad MIME type for the binary, - // in which case falling back to ArrayBuffer instantiation should work. - err(`wasm streaming compile failed: ${reason}`); - err('falling back to ArrayBuffer instantiation'); - return instantiateArrayBuffer(binaryFile, imports, callback); - }); - }); + && !ENVIRONMENT_IS_NODE + ) { + try { + var response = fetch(binaryFile, { credentials: 'same-origin' }); + var instantiationResult = await WebAssembly.instantiateStreaming(response, imports); + return instantiationResult; + } catch (reason) { + // We expect the most common failure cause to be a bad MIME type for the binary, + // in which case falling back to ArrayBuffer instantiation should work. + err(`wasm streaming compile failed: ${reason}`); + err('falling back to ArrayBuffer instantiation'); + // fall back of instantiateArrayBuffer below + }; } - return instantiateArrayBuffer(binaryFile, imports, callback); + return instantiateArrayBuffer(binaryFile, imports); } -// Create the wasm instance. -// Receives the wasm imports, returns the exports. -function createWasm() { +function getWasmImports() { // prepare imports - var info = { + var imports = { 'env': wasmImports, 'wasi_snapshot_preview1': wasmImports, }; + return imports; +} + +// Create the wasm instance. +// Receives the wasm imports, returns the exports. +async function createWasm() { // Load the wasm module and create an instance of using native support in the JS engine. // handle a generated wasm instance, receiving its exports and // performing other necessary setup @@ -833,23 +680,13 @@ function createWasm() { function receiveInstance(instance, module) { wasmExports = instance.exports; - + assignWasmExports(wasmExports); - wasmMemory = wasmExports['memory']; - - assert(wasmMemory, "memory not found in wasm exports"); - // This assertion doesn't hold when emscripten is run in --post-link - // mode. - // TODO(sbc): Read INITIAL_MEMORY out of the wasm file in post-link mode. - //assert(wasmMemory.buffer.byteLength === 16777216); updateMemoryViews(); - addOnInit(wasmExports['__wasm_call_ctors']); - removeRunDependency('wasm-instantiate'); return wasmExports; } - // wait for the pthread pool (if any) addRunDependency('wasm-instantiate'); // Prefer streaming instantiation if available. @@ -864,9 +701,11 @@ function createWasm() { trueModule = null; // TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193, the above line no longer optimizes out down to the following line. // When the regression is fixed, can restore the above PTHREADS-enabled path. - receiveInstance(result['instance']); + return receiveInstance(result['instance']); } + var info = getWasmImports(); + // User shell pages can write their own Module.instantiateWasm = function(imports, successCallback) callback // to manually instantiate the Wasm module themselves. This allows pages to // run the instantiation parallel to any other async startup actions they are @@ -874,132 +713,66 @@ function createWasm() { // Also pthreads and wasm workers initialize the wasm instance through this // path. if (Module['instantiateWasm']) { - - try { - return Module['instantiateWasm'](info, receiveInstance); - } catch(e) { - err(`Module.instantiateWasm callback failed with error: ${e}`); - return false; - } + return new Promise((resolve, reject) => { + try { + Module['instantiateWasm'](info, (inst, mod) => { + resolve(receiveInstance(inst, mod)); + }); + } catch(e) { + err(`Module.instantiateWasm callback failed with error: ${e}`); + reject(e); + } + }); } - instantiateAsync(wasmBinary, wasmBinaryFile, info, receiveInstantiationResult); - return {}; // no exports yet; we'll fill them in later + wasmBinaryFile ??= findWasmBinary(); + var result = await instantiateAsync(wasmBinary, wasmBinaryFile, info); + var exports = receiveInstantiationResult(result); + return exports; } -// Globals used by JS i64 conversions (see makeSetValue) -var tempDouble; -var tempI64; +// end include: preamble.js -// include: runtime_debug.js -function legacyModuleProp(prop, newName, incomming=true) { - if (!Object.getOwnPropertyDescriptor(Module, prop)) { - Object.defineProperty(Module, prop, { - configurable: true, - get() { - let extra = incomming ? ' (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)' : ''; - abort(`\`Module.${prop}\` has been replaced by \`${newName}\`` + extra); +// Begin JS library code + + class ExitStatus { + name = 'ExitStatus'; + constructor(status) { + this.message = `Program terminated with exit(${status})`; + this.status = status; } - }); - } -} + } -function ignoredModuleProp(prop) { - if (Object.getOwnPropertyDescriptor(Module, prop)) { - abort(`\`Module.${prop}\` was supplied but \`${prop}\` not included in INCOMING_MODULE_JS_API`); - } -} + /** @type {!Int16Array} */ + var HEAP16; -// forcing the filesystem exports a few things by default -function isExportedByForceFilesystem(name) { - return name === 'FS_createPath' || - name === 'FS_createDataFile' || - name === 'FS_createPreloadedFile' || - name === 'FS_unlink' || - name === 'addRunDependency' || - // The old FS has some functionality that WasmFS lacks. - name === 'FS_createLazyFile' || - name === 'FS_createDevice' || - name === 'removeRunDependency'; -} + /** @type {!Int32Array} */ + var HEAP32; -function missingGlobal(sym, msg) { - if (typeof globalThis !== 'undefined') { - Object.defineProperty(globalThis, sym, { - configurable: true, - get() { - warnOnce('`' + sym + '` is not longer defined by emscripten. ' + msg); - return undefined; - } - }); - } -} + /** not-@type {!BigInt64Array} */ + var HEAP64; -missingGlobal('buffer', 'Please use HEAP8.buffer or wasmMemory.buffer'); -missingGlobal('asm', 'Please use wasmExports instead'); + /** @type {!Int8Array} */ + var HEAP8; -function missingLibrarySymbol(sym) { - if (typeof globalThis !== 'undefined' && !Object.getOwnPropertyDescriptor(globalThis, sym)) { - Object.defineProperty(globalThis, sym, { - configurable: true, - get() { - // Can't `abort()` here because it would break code that does runtime - // checks. e.g. `if (typeof SDL === 'undefined')`. - var msg = '`' + sym + '` is a library symbol and not included by default; add it to your library.js __deps or to DEFAULT_LIBRARY_FUNCS_TO_INCLUDE on the command line'; - // DEFAULT_LIBRARY_FUNCS_TO_INCLUDE requires the name as it appears in - // library.js, which means $name for a JS name with no prefix, or name - // for a JS name like _name. - var librarySymbol = sym; - if (!librarySymbol.startsWith('_')) { - librarySymbol = '$' + sym; - } - msg += " (e.g. -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='" + librarySymbol + "')"; - if (isExportedByForceFilesystem(sym)) { - msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you'; - } - warnOnce(msg); - return undefined; - } - }); - } - // Any symbol that is not included from the JS libary is also (by definition) - // not exported on the Module object. - unexportedRuntimeSymbol(sym); -} + /** @type {!Float32Array} */ + var HEAPF32; -function unexportedRuntimeSymbol(sym) { - if (!Object.getOwnPropertyDescriptor(Module, sym)) { - Object.defineProperty(Module, sym, { - configurable: true, - get() { - var msg = "'" + sym + "' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the Emscripten FAQ)"; - if (isExportedByForceFilesystem(sym)) { - msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you'; - } - abort(msg); - } - }); - } -} + /** @type {!Float64Array} */ + var HEAPF64; -// Used by XXXXX_DEBUG settings to output debug messages. -function dbg(text) { - // TODO(sbc): Make this configurable somehow. Its not always convenient for - // logging to show up as warnings. - console.warn.apply(console, arguments); -} -// end include: runtime_debug.js -// === Body === + /** @type {!Uint16Array} */ + var HEAPU16; -// end include: preamble.js + /** @type {!Uint32Array} */ + var HEAPU32; - /** @constructor */ - function ExitStatus(status) { - this.name = 'ExitStatus'; - this.message = `Program terminated with exit(${status})`; - this.status = status; - } + /** not-@type {!BigUint64Array} */ + var HEAPU64; + + /** @type {!Uint8Array} */ + var HEAPU8; var callRuntimeCallbacks = (callbacks) => { while (callbacks.length > 0) { @@ -1007,20 +780,91 @@ function dbg(text) { callbacks.shift()(Module); } }; + var onPostRuns = []; + var addOnPostRun = (cb) => onPostRuns.push(cb); + + var onPreRuns = []; + var addOnPreRun = (cb) => onPreRuns.push(cb); + + var runDependencies = 0; + + + var dependenciesFulfilled = null; + + var runDependencyTracking = { + }; + + var runDependencyWatcher = null; + var removeRunDependency = (id) => { + runDependencies--; + + Module['monitorRunDependencies']?.(runDependencies); + + assert(id, 'removeRunDependency requires an ID'); + assert(runDependencyTracking[id]); + delete runDependencyTracking[id]; + if (runDependencies == 0) { + if (runDependencyWatcher !== null) { + clearInterval(runDependencyWatcher); + runDependencyWatcher = null; + } + if (dependenciesFulfilled) { + var callback = dependenciesFulfilled; + dependenciesFulfilled = null; + callback(); // can add another dependenciesFulfilled + } + } + }; + + + var addRunDependency = (id) => { + runDependencies++; + + Module['monitorRunDependencies']?.(runDependencies); + + assert(id, 'addRunDependency requires an ID') + assert(!runDependencyTracking[id]); + runDependencyTracking[id] = 1; + if (runDependencyWatcher === null && globalThis.setInterval) { + // Check for missing dependencies every few seconds + runDependencyWatcher = setInterval(() => { + if (ABORT) { + clearInterval(runDependencyWatcher); + runDependencyWatcher = null; + return; + } + var shown = false; + for (var dep in runDependencyTracking) { + if (!shown) { + shown = true; + err('still waiting on run dependencies:'); + } + err(`dependency: ${dep}`); + } + if (shown) { + err('(end of list)'); + } + }, 10000); + // Prevent this timer from keeping the runtime alive if nothing + // else is. + runDependencyWatcher.unref?.() + } + }; + /** - * @param {number} ptr - * @param {string} type - */ + * @param {number} ptr + * @param {string} type + */ function getValue(ptr, type = 'i8') { if (type.endsWith('*')) type = '*'; switch (type) { - case 'i1': return HEAP8[((ptr)>>0)]; - case 'i8': return HEAP8[((ptr)>>0)]; + case 'i1': return HEAP8[ptr]; + case 'i8': return HEAP8[ptr]; case 'i16': return HEAP16[((ptr)>>1)]; case 'i32': return HEAP32[((ptr)>>2)]; - case 'i64': abort('to do getValue(i64) use WASM_BIGINT'); + case 'i64': return HEAP64[((ptr)>>3)]; case 'float': return HEAPF32[((ptr)>>2)]; case 'double': return HEAPF64[((ptr)>>3)]; case '*': return HEAPU32[((ptr)>>2)]; @@ -1028,29 +872,30 @@ function dbg(text) { } } - var noExitRuntime = Module['noExitRuntime'] || true; + var noExitRuntime = true; - var ptrToString = (ptr) => { - assert(typeof ptr === 'number'); - // With CAN_ADDRESS_2GB or MEMORY64, pointers are already unsigned. + function ptrToString(ptr) { + assert(typeof ptr === 'number', `ptrToString expects a number, got ${typeof ptr}`); + // Convert to 32-bit unsigned value ptr >>>= 0; return '0x' + ptr.toString(16).padStart(8, '0'); - }; + } + /** - * @param {number} ptr - * @param {number} value - * @param {string} type - */ + * @param {number} ptr + * @param {number} value + * @param {string} type + */ function setValue(ptr, value, type = 'i8') { if (type.endsWith('*')) type = '*'; switch (type) { - case 'i1': HEAP8[((ptr)>>0)] = value; break; - case 'i8': HEAP8[((ptr)>>0)] = value; break; + case 'i1': HEAP8[ptr] = value; break; + case 'i8': HEAP8[ptr] = value; break; case 'i16': HEAP16[((ptr)>>1)] = value; break; case 'i32': HEAP32[((ptr)>>2)] = value; break; - case 'i64': abort('to do setValue(i64) use WASM_BIGINT'); + case 'i64': HEAP64[((ptr)>>3)] = BigInt(value); break; case 'float': HEAPF32[((ptr)>>2)] = value; break; case 'double': HEAPF64[((ptr)>>3)] = value; break; case '*': HEAPU32[((ptr)>>2)] = value; break; @@ -1058,8 +903,12 @@ function dbg(text) { } } + var stackRestore = (val) => __emscripten_stack_restore(val); + + var stackSave = () => _emscripten_stack_get_current(); + var warnOnce = (text) => { - if (!warnOnce.shown) warnOnce.shown = {}; + warnOnce.shown ||= {}; if (!warnOnce.shown[text]) { warnOnce.shown[text] = 1; if (ENVIRONMENT_IS_NODE) text = 'warning: ' + text; @@ -1067,6 +916,8 @@ function dbg(text) { } }; + + var PATH = { isAbs:(path) => path.charAt(0) === '/', splitPath:(filename) => { @@ -1098,7 +949,7 @@ function dbg(text) { }, normalize:(path) => { var isAbsolute = PATH.isAbs(path), - trailingSlash = path.substr(-1) === '/'; + trailingSlash = path.slice(-1) === '/'; // Normalize the path path = PATH.normalizeArray(path.split('/').filter((p) => !!p), !isAbsolute).join('/'); if (!path && !isAbsolute) { @@ -1119,143 +970,116 @@ function dbg(text) { } if (dir) { // It has a dirname, strip trailing slash - dir = dir.substr(0, dir.length - 1); + dir = dir.slice(0, -1); } return root + dir; }, - basename:(path) => { - // EMSCRIPTEN return '/'' for '/', not an empty string - if (path === '/') return '/'; - path = PATH.normalize(path); - path = path.replace(/\/$/, ""); - var lastSlash = path.lastIndexOf('/'); - if (lastSlash === -1) return path; - return path.substr(lastSlash+1); - }, - join:function() { - var paths = Array.prototype.slice.call(arguments); - return PATH.normalize(paths.join('/')); - }, - join2:(l, r) => PATH.normalize(l + '/' + r), + basename:(path) => path && path.match(/([^\/]+|\/)\/*$/)[1], +join:(...paths) => PATH.normalize(paths.join('/')), +join2:(l, r) => PATH.normalize(l + '/' + r), +}; + +var initRandomFill = () => { + // This block is not needed on v19+ since crypto.getRandomValues is builtin + if (ENVIRONMENT_IS_NODE) { + var nodeCrypto = require('node:crypto'); + return (view) => nodeCrypto.randomFillSync(view); + } + + return (view) => (crypto.getRandomValues(view), 0); }; - - var initRandomFill = () => { - if (typeof crypto == 'object' && typeof crypto['getRandomValues'] == 'function') { - // for modern web browsers - return (view) => crypto.getRandomValues(view); - } else - if (ENVIRONMENT_IS_NODE) { - // for nodejs with or without crypto support included - try { - var crypto_module = require('crypto'); - var randomFillSync = crypto_module['randomFillSync']; - if (randomFillSync) { - // nodejs with LTS crypto support - return (view) => crypto_module['randomFillSync'](view); - } - // very old nodejs with the original crypto API - var randomBytes = crypto_module['randomBytes']; - return (view) => ( - view.set(randomBytes(view.byteLength)), - // Return the original view to match modern native implementations. - view - ); - } catch (e) { - // nodejs doesn't have crypto support +var randomFill = (view) => (randomFill = initRandomFill())(view); + + + +var PATH_FS = { +resolve:(...args) => { + var resolvedPath = '', + resolvedAbsolute = false; + for (var i = args.length - 1; i >= -1 && !resolvedAbsolute; i--) { + var path = (i >= 0) ? args[i] : FS.cwd(); + // Skip empty and invalid entries + if (typeof path != 'string') { + throw new TypeError('Arguments to path.resolve must be strings'); + } else if (!path) { + return ''; // an invalid portion invalidates the whole thing } + resolvedPath = path + '/' + resolvedPath; + resolvedAbsolute = PATH.isAbs(path); } - // we couldn't find a proper implementation, as Math.random() is not suitable for /dev/random, see emscripten-core/emscripten/pull/7096 - abort("no cryptographic support found for randomDevice. consider polyfilling it if you want to use something insecure like Math.random(), e.g. put this in a --pre-js: var crypto = { getRandomValues: (array) => { for (var i = 0; i < array.length; i++) array[i] = (Math.random()*256)|0 } };"); - }; - var randomFill = (view) => { - // Lazily init on the first invocation. - return (randomFill = initRandomFill())(view); - }; - - - - var PATH_FS = { - resolve:function() { - var resolvedPath = '', - resolvedAbsolute = false; - for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { - var path = (i >= 0) ? arguments[i] : FS.cwd(); - // Skip empty and invalid entries - if (typeof path != 'string') { - throw new TypeError('Arguments to path.resolve must be strings'); - } else if (!path) { - return ''; // an invalid portion invalidates the whole thing - } - resolvedPath = path + '/' + resolvedPath; - resolvedAbsolute = PATH.isAbs(path); - } - // At this point the path should be resolved to a full absolute path, but - // handle relative paths to be safe (might happen when process.cwd() fails) - resolvedPath = PATH.normalizeArray(resolvedPath.split('/').filter((p) => !!p), !resolvedAbsolute).join('/'); - return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; - }, - relative:(from, to) => { - from = PATH_FS.resolve(from).substr(1); - to = PATH_FS.resolve(to).substr(1); - function trim(arr) { - var start = 0; - for (; start < arr.length; start++) { - if (arr[start] !== '') break; - } - var end = arr.length - 1; - for (; end >= 0; end--) { - if (arr[end] !== '') break; - } - if (start > end) return []; - return arr.slice(start, end - start + 1); - } - var fromParts = trim(from.split('/')); - var toParts = trim(to.split('/')); - var length = Math.min(fromParts.length, toParts.length); - var samePartsLength = length; - for (var i = 0; i < length; i++) { - if (fromParts[i] !== toParts[i]) { - samePartsLength = i; - break; - } - } - var outputParts = []; - for (var i = samePartsLength; i < fromParts.length; i++) { - outputParts.push('..'); + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + resolvedPath = PATH.normalizeArray(resolvedPath.split('/').filter((p) => !!p), !resolvedAbsolute).join('/'); + return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; + }, +relative:(from, to) => { + from = PATH_FS.resolve(from).slice(1); + to = PATH_FS.resolve(to).slice(1); + function trim(arr) { + var start = 0; + for (; start < arr.length; start++) { + if (arr[start] !== '') break; + } + var end = arr.length - 1; + for (; end >= 0; end--) { + if (arr[end] !== '') break; + } + if (start > end) return []; + return arr.slice(start, end - start + 1); + } + var fromParts = trim(from.split('/')); + var toParts = trim(to.split('/')); + var length = Math.min(fromParts.length, toParts.length); + var samePartsLength = length; + for (var i = 0; i < length; i++) { + if (fromParts[i] !== toParts[i]) { + samePartsLength = i; + break; } - outputParts = outputParts.concat(toParts.slice(samePartsLength)); - return outputParts.join('/'); - }, + } + var outputParts = []; + for (var i = samePartsLength; i < fromParts.length; i++) { + outputParts.push('..'); + } + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + return outputParts.join('/'); + }, +}; + + +var UTF8Decoder = globalThis.TextDecoder && new TextDecoder(); + +var findStringEnd = (heapOrArray, idx, maxBytesToRead, ignoreNul) => { + var maxIdx = idx + maxBytesToRead; + if (ignoreNul) return maxIdx; + // TextDecoder needs to know the byte length in advance, it doesn't stop on + // null terminator by itself. + // As a tiny code save trick, compare idx against maxIdx using a negation, + // so that maxBytesToRead=undefined/NaN means Infinity. + while (heapOrArray[idx] && !(idx >= maxIdx)) ++idx; + return idx; }; + + + /** + * Given a pointer 'idx' to a null-terminated UTF8-encoded string in the given + * array that contains uint8 values, returns a copy of that string as a + * Javascript String object. + * heapOrArray is either a regular array, or a JavaScript typed array view. + * @param {number=} idx + * @param {number=} maxBytesToRead + * @param {boolean=} ignoreNul - If true, the function will not stop on a NUL character. + * @return {string} + */ + var UTF8ArrayToString = (heapOrArray, idx = 0, maxBytesToRead, ignoreNul) => { + var endPtr = findStringEnd(heapOrArray, idx, maxBytesToRead, ignoreNul); - var UTF8Decoder = typeof TextDecoder != 'undefined' ? new TextDecoder('utf8') : undefined; - - /** - * Given a pointer 'idx' to a null-terminated UTF8-encoded string in the given - * array that contains uint8 values, returns a copy of that string as a - * Javascript String object. - * heapOrArray is either a regular array, or a JavaScript typed array view. - * @param {number} idx - * @param {number=} maxBytesToRead - * @return {string} - */ - var UTF8ArrayToString = (heapOrArray, idx, maxBytesToRead) => { - var endIdx = idx + maxBytesToRead; - var endPtr = idx; - // TextDecoder needs to know the byte length in advance, it doesn't stop on - // null terminator by itself. Also, use the length info to avoid running tiny - // strings through TextDecoder, since .subarray() allocates garbage. - // (As a tiny code save trick, compare endPtr against endIdx using a negation, - // so that undefined means Infinity) - while (heapOrArray[endPtr] && !(endPtr >= endIdx)) ++endPtr; - + // When using conditional TextDecoder, skip it for short strings as the overhead of the native call is not worth it. if (endPtr - idx > 16 && heapOrArray.buffer && UTF8Decoder) { return UTF8Decoder.decode(heapOrArray.subarray(idx, endPtr)); } var str = ''; - // If building with TextDecoder, we have already computed the string length - // above, so test loop end condition against that while (idx < endPtr) { // For UTF8 byte structure, see: // http://en.wikipedia.org/wiki/UTF-8#Description @@ -1269,7 +1093,7 @@ function dbg(text) { if ((u0 & 0xF0) == 0xE0) { u0 = ((u0 & 15) << 12) | (u1 << 6) | u2; } else { - if ((u0 & 0xF8) != 0xF0) warnOnce('Invalid UTF-8 leading byte ' + ptrToString(u0) + ' encountered when deserializing a UTF-8 string in wasm memory to a JS string!'); + if ((u0 & 0xF8) != 0xF0) warnOnce(`Invalid UTF-8 leading byte ${ptrToString(u0)} encountered when deserializing a UTF-8 string in wasm memory to a JS string!`); u0 = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | (heapOrArray[idx++] & 63); } @@ -1316,18 +1140,10 @@ function dbg(text) { var startIdx = outIdx; var endIdx = outIdx + maxBytesToWrite - 1; // -1 for string null terminator. for (var i = 0; i < str.length; ++i) { - // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code - // unit, not a Unicode code point of the character! So decode - // UTF16->UTF32->UTF8. - // See http://unicode.org/faq/utf_bom.html#utf16-3 // For UTF8 byte structure, see http://en.wikipedia.org/wiki/UTF-8#Description // and https://www.ietf.org/rfc/rfc2279.txt // and https://tools.ietf.org/html/rfc3629 - var u = str.charCodeAt(i); // possibly a lead surrogate - if (u >= 0xD800 && u <= 0xDFFF) { - var u1 = str.charCodeAt(++i); - u = 0x10000 + ((u & 0x3FF) << 10) | (u1 & 0x3FF); - } + var u = str.codePointAt(i); if (u <= 0x7F) { if (outIdx >= endIdx) break; heap[outIdx++] = u; @@ -1342,11 +1158,14 @@ function dbg(text) { heap[outIdx++] = 0x80 | (u & 63); } else { if (outIdx + 3 >= endIdx) break; - if (u > 0x10FFFF) warnOnce('Invalid Unicode code point ' + ptrToString(u) + ' encountered when serializing a JS string to a UTF-8 string in wasm memory! (Valid unicode code points should be in range 0-0x10FFFF).'); + if (u > 0x10FFFF) warnOnce(`Invalid Unicode code point ${ptrToString(u)} encountered when serializing a JS string to a UTF-8 string in wasm memory! (Valid unicode code points should be in range 0-0x10FFFF).`); heap[outIdx++] = 0xF0 | (u >> 18); heap[outIdx++] = 0x80 | ((u >> 12) & 63); heap[outIdx++] = 0x80 | ((u >> 6) & 63); heap[outIdx++] = 0x80 | (u & 63); + // Gotcha: if codePoint is over 0xFFFF, it is represented as a surrogate pair in UTF-16. + // We need to manually skip over the second code unit for correct iteration. + i++; } } // Null-terminate the pointer to the buffer. @@ -1354,13 +1173,13 @@ function dbg(text) { return outIdx - startIdx; }; /** @type {function(string, boolean=, number=)} */ - function intArrayFromString(stringy, dontAddNull, length) { - var len = length > 0 ? length : lengthBytesUTF8(stringy)+1; - var u8array = new Array(len); - var numBytesWritten = stringToUTF8Array(stringy, u8array, 0, u8array.length); - if (dontAddNull) u8array.length = numBytesWritten; - return u8array; - } + var intArrayFromString = (stringy, dontAddNull, length) => { + var len = length > 0 ? length : lengthBytesUTF8(stringy)+1; + var u8array = new Array(len); + var numBytesWritten = stringToUTF8Array(stringy, u8array, 0, u8array.length); + if (dontAddNull) u8array.length = numBytesWritten; + return u8array; + }; var FS_stdin_getChar = () => { if (!FS_stdin_getChar_buffer.length) { var result = null; @@ -1380,34 +1199,27 @@ function dbg(text) { var fd = process.stdin.fd; try { - bytesRead = fs.readSync(fd, buf); + bytesRead = fs.readSync(fd, buf, 0, BUFSIZE); } catch(e) { - // Cross-platform differences: on Windows, reading EOF throws an exception, but on other OSes, - // reading EOF returns 0. Uniformize behavior by treating the EOF exception to return 0. + // Cross-platform differences: on Windows, reading EOF throws an + // exception, but on other OSes, reading EOF returns 0. Uniformize + // behavior by treating the EOF exception to return 0. if (e.toString().includes('EOF')) bytesRead = 0; else throw e; } if (bytesRead > 0) { result = buf.slice(0, bytesRead).toString('utf-8'); - } else { - result = null; } } else - if (typeof window != 'undefined' && - typeof window.prompt == 'function') { + if (globalThis.window?.prompt) { // Browser. result = window.prompt('Input: '); // returns null on cancel if (result !== null) { result += '\n'; } - } else if (typeof readline == 'function') { - // Command line. - result = readline(); - if (result !== null) { - result += '\n'; - } - } + } else + {} if (!result) { return null; } @@ -1478,7 +1290,7 @@ function dbg(text) { buffer[offset+i] = result; } if (bytesRead) { - stream.node.timestamp = Date.now(); + stream.node.atime = Date.now(); } return bytesRead; }, @@ -1494,7 +1306,7 @@ function dbg(text) { throw new FS.ErrnoError(29); } if (length) { - stream.node.timestamp = Date.now(); + stream.node.mtime = stream.node.ctime = Date.now(); } return i; }, @@ -1505,15 +1317,15 @@ function dbg(text) { }, put_char(tty, val) { if (val === null || val === 10) { - out(UTF8ArrayToString(tty.output, 0)); + out(UTF8ArrayToString(tty.output)); tty.output = []; } else { if (val != 0) tty.output.push(val); // val == 0 would cut text output off in the middle. } }, fsync(tty) { - if (tty.output && tty.output.length > 0) { - out(UTF8ArrayToString(tty.output, 0)); + if (tty.output?.length > 0) { + out(UTF8ArrayToString(tty.output)); tty.output = []; } }, @@ -1542,15 +1354,15 @@ function dbg(text) { default_tty1_ops:{ put_char(tty, val) { if (val === null || val === 10) { - err(UTF8ArrayToString(tty.output, 0)); + err(UTF8ArrayToString(tty.output)); tty.output = []; } else { if (val != 0) tty.output.push(val); } }, fsync(tty) { - if (tty.output && tty.output.length > 0) { - err(UTF8ArrayToString(tty.output, 0)); + if (tty.output?.length > 0) { + err(UTF8ArrayToString(tty.output)); tty.output = []; } }, @@ -1558,77 +1370,65 @@ function dbg(text) { }; - var zeroMemory = (address, size) => { - HEAPU8.fill(0, address, address + size); - return address; - }; - - var alignMemory = (size, alignment) => { - assert(alignment, "alignment argument is required"); - return Math.ceil(size / alignment) * alignment; - }; var mmapAlloc = (size) => { abort('internal error: mmapAlloc called but `emscripten_builtin_memalign` native symbol not exported'); }; var MEMFS = { ops_table:null, mount(mount) { - return MEMFS.createNode(null, '/', 16384 | 511 /* 0777 */, 0); + return MEMFS.createNode(null, '/', 16895, 0); }, createNode(parent, name, mode, dev) { if (FS.isBlkdev(mode) || FS.isFIFO(mode)) { - // no supported + // not supported throw new FS.ErrnoError(63); } - if (!MEMFS.ops_table) { - MEMFS.ops_table = { - dir: { - node: { - getattr: MEMFS.node_ops.getattr, - setattr: MEMFS.node_ops.setattr, - lookup: MEMFS.node_ops.lookup, - mknod: MEMFS.node_ops.mknod, - rename: MEMFS.node_ops.rename, - unlink: MEMFS.node_ops.unlink, - rmdir: MEMFS.node_ops.rmdir, - readdir: MEMFS.node_ops.readdir, - symlink: MEMFS.node_ops.symlink - }, - stream: { - llseek: MEMFS.stream_ops.llseek - } + MEMFS.ops_table ||= { + dir: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr, + lookup: MEMFS.node_ops.lookup, + mknod: MEMFS.node_ops.mknod, + rename: MEMFS.node_ops.rename, + unlink: MEMFS.node_ops.unlink, + rmdir: MEMFS.node_ops.rmdir, + readdir: MEMFS.node_ops.readdir, + symlink: MEMFS.node_ops.symlink }, - file: { - node: { - getattr: MEMFS.node_ops.getattr, - setattr: MEMFS.node_ops.setattr - }, - stream: { - llseek: MEMFS.stream_ops.llseek, - read: MEMFS.stream_ops.read, - write: MEMFS.stream_ops.write, - allocate: MEMFS.stream_ops.allocate, - mmap: MEMFS.stream_ops.mmap, - msync: MEMFS.stream_ops.msync - } - }, - link: { - node: { - getattr: MEMFS.node_ops.getattr, - setattr: MEMFS.node_ops.setattr, - readlink: MEMFS.node_ops.readlink - }, - stream: {} + stream: { + llseek: MEMFS.stream_ops.llseek + } + }, + file: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr }, - chrdev: { - node: { - getattr: MEMFS.node_ops.getattr, - setattr: MEMFS.node_ops.setattr - }, - stream: FS.chrdev_stream_ops + stream: { + llseek: MEMFS.stream_ops.llseek, + read: MEMFS.stream_ops.read, + write: MEMFS.stream_ops.write, + mmap: MEMFS.stream_ops.mmap, + msync: MEMFS.stream_ops.msync } - }; - } + }, + link: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr, + readlink: MEMFS.node_ops.readlink + }, + stream: {} + }, + chrdev: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr + }, + stream: FS.chrdev_stream_ops + } + }; var node = FS.createNode(parent, name, mode, dev); if (FS.isDir(node.mode)) { node.node_ops = MEMFS.ops_table.dir.node; @@ -1637,11 +1437,14 @@ function dbg(text) { } else if (FS.isFile(node.mode)) { node.node_ops = MEMFS.ops_table.file.node; node.stream_ops = MEMFS.ops_table.file.stream; - node.usedBytes = 0; // The actual number of bytes used in the typed array, as opposed to contents.length which gives the whole capacity. - // When the byte data of the file is populated, this will point to either a typed array, or a normal JS array. Typed arrays are preferred - // for performance, and used by default. However, typed arrays are not resizable like normal JS arrays are, so there is a small disk size - // penalty involved for appending file writes that continuously grow a file similar to std::vector capacity vs used -scheme. - node.contents = null; + // The actual number of bytes used in the typed array, as opposed to + // contents.length which gives the whole capacity. + node.usedBytes = 0; + // The byte data of the file is stored in a typed array. + // Note: typed arrays are not resizable like normal JS arrays are, so + // there is a small penalty involved for appending file writes that + // continuously grow a file similar to std::vector capacity vs used. + node.contents = MEMFS.emptyFileContents ??= new Uint8Array(0); } else if (FS.isLink(node.mode)) { node.node_ops = MEMFS.ops_table.link.node; node.stream_ops = MEMFS.ops_table.link.stream; @@ -1649,45 +1452,39 @@ function dbg(text) { node.node_ops = MEMFS.ops_table.chrdev.node; node.stream_ops = MEMFS.ops_table.chrdev.stream; } - node.timestamp = Date.now(); + node.atime = node.mtime = node.ctime = Date.now(); // add the new node to the parent if (parent) { parent.contents[name] = node; - parent.timestamp = node.timestamp; + parent.atime = parent.mtime = parent.ctime = node.atime; } return node; }, getFileDataAsTypedArray(node) { - if (!node.contents) return new Uint8Array(0); - if (node.contents.subarray) return node.contents.subarray(0, node.usedBytes); // Make sure to not return excess unused bytes. - return new Uint8Array(node.contents); + assert(FS.isFile(node.mode), 'getFileDataAsTypedArray called on non-file'); + return node.contents.subarray(0, node.usedBytes); // Make sure to not return excess unused bytes. }, expandFileStorage(node, newCapacity) { - var prevCapacity = node.contents ? node.contents.length : 0; + var prevCapacity = node.contents.length; if (prevCapacity >= newCapacity) return; // No need to expand, the storage was already large enough. - // Don't expand strictly to the given requested limit if it's only a very small increase, but instead geometrically grow capacity. - // For small filesizes (<1MB), perform size*2 geometric increase, but for large sizes, do a much more conservative size*1.125 increase to - // avoid overshooting the allocation cap by a very large margin. + // Don't expand strictly to the given requested limit if it's only a very + // small increase, but instead geometrically grow capacity. + // For small filesizes (<1MB), perform size*2 geometric increase, but for + // large sizes, do a much more conservative size*1.125 increase to avoid + // overshooting the allocation cap by a very large margin. var CAPACITY_DOUBLING_MAX = 1024 * 1024; newCapacity = Math.max(newCapacity, (prevCapacity * (prevCapacity < CAPACITY_DOUBLING_MAX ? 2.0 : 1.125)) >>> 0); - if (prevCapacity != 0) newCapacity = Math.max(newCapacity, 256); // At minimum allocate 256b for each file when expanding. - var oldContents = node.contents; + if (prevCapacity) newCapacity = Math.max(newCapacity, 256); // At minimum allocate 256b for each file when expanding. + var oldContents = MEMFS.getFileDataAsTypedArray(node); node.contents = new Uint8Array(newCapacity); // Allocate new storage. - if (node.usedBytes > 0) node.contents.set(oldContents.subarray(0, node.usedBytes), 0); // Copy old data over to the new storage. + node.contents.set(oldContents); }, resizeFileStorage(node, newSize) { if (node.usedBytes == newSize) return; - if (newSize == 0) { - node.contents = null; // Fully decommit when requesting a resize to zero. - node.usedBytes = 0; - } else { - var oldContents = node.contents; - node.contents = new Uint8Array(newSize); // Allocate new storage. - if (oldContents) { - node.contents.set(oldContents.subarray(0, Math.min(newSize, node.usedBytes))); // Copy old data over to the new storage. - } - node.usedBytes = newSize; - } + var oldContents = node.contents; + node.contents = new Uint8Array(newSize); // Allocate new storage. + node.contents.set(oldContents.subarray(0, Math.min(newSize, node.usedBytes))); // Copy old data over to the new storage. + node.usedBytes = newSize; }, node_ops:{ getattr(node) { @@ -1709,9 +1506,9 @@ function dbg(text) { } else { attr.size = 0; } - attr.atime = new Date(node.timestamp); - attr.mtime = new Date(node.timestamp); - attr.ctime = new Date(node.timestamp); + attr.atime = new Date(node.atime); + attr.mtime = new Date(node.mtime); + attr.ctime = new Date(node.ctime); // NOTE: In our implementation, st_blocks = Math.ceil(st_size/st_blksize), // but this is not required by the standard. attr.blksize = 4096; @@ -1719,47 +1516,44 @@ function dbg(text) { return attr; }, setattr(node, attr) { - if (attr.mode !== undefined) { - node.mode = attr.mode; - } - if (attr.timestamp !== undefined) { - node.timestamp = attr.timestamp; + for (const key of ["mode", "atime", "mtime", "ctime"]) { + if (attr[key] != null) { + node[key] = attr[key]; + } } if (attr.size !== undefined) { MEMFS.resizeFileStorage(node, attr.size); } }, lookup(parent, name) { - throw FS.genericErrors[44]; + throw new FS.ErrnoError(44); }, mknod(parent, name, mode, dev) { return MEMFS.createNode(parent, name, mode, dev); }, rename(old_node, new_dir, new_name) { - // if we're overwriting a directory at new_name, make sure it's empty. - if (FS.isDir(old_node.mode)) { - var new_node; - try { - new_node = FS.lookupNode(new_dir, new_name); - } catch (e) { - } - if (new_node) { + var new_node; + try { + new_node = FS.lookupNode(new_dir, new_name); + } catch (e) {} + if (new_node) { + if (FS.isDir(old_node.mode)) { + // if we're overwriting a directory at new_name, make sure it's empty. for (var i in new_node.contents) { throw new FS.ErrnoError(55); } } + FS.hashRemoveNode(new_node); } // do the internal rewiring delete old_node.parent.contents[old_node.name]; - old_node.parent.timestamp = Date.now() - old_node.name = new_name; new_dir.contents[new_name] = old_node; - new_dir.timestamp = old_node.parent.timestamp; - old_node.parent = new_dir; + old_node.name = new_name; + new_dir.ctime = new_dir.mtime = old_node.parent.ctime = old_node.parent.mtime = Date.now(); }, unlink(parent, name) { delete parent.contents[name]; - parent.timestamp = Date.now(); + parent.ctime = parent.mtime = Date.now(); }, rmdir(parent, name) { var node = FS.lookupNode(parent, name); @@ -1767,20 +1561,13 @@ function dbg(text) { throw new FS.ErrnoError(55); } delete parent.contents[name]; - parent.timestamp = Date.now(); + parent.ctime = parent.mtime = Date.now(); }, readdir(node) { - var entries = ['.', '..']; - for (var key in node.contents) { - if (!node.contents.hasOwnProperty(key)) { - continue; - } - entries.push(key); - } - return entries; + return ['.', '..', ...Object.keys(node.contents)]; }, symlink(parent, newname, oldpath) { - var node = MEMFS.createNode(parent, newname, 511 /* 0777 */ | 40960, 0); + var node = MEMFS.createNode(parent, newname, 0o777 | 40960, 0); node.link = oldpath; return node; }, @@ -1797,48 +1584,29 @@ function dbg(text) { if (position >= stream.node.usedBytes) return 0; var size = Math.min(stream.node.usedBytes - position, length); assert(size >= 0); - if (size > 8 && contents.subarray) { // non-trivial, and typed array - buffer.set(contents.subarray(position, position + size), offset); - } else { - for (var i = 0; i < size; i++) buffer[offset + i] = contents[position + i]; - } + buffer.set(contents.subarray(position, position + size), offset); return size; }, write(stream, buffer, offset, length, position, canOwn) { - // The data buffer should be a typed array view - assert(!(buffer instanceof ArrayBuffer)); + assert(buffer.subarray, 'FS.write expects a TypedArray'); if (!length) return 0; var node = stream.node; - node.timestamp = Date.now(); - - if (buffer.subarray && (!node.contents || node.contents.subarray)) { // This write is from a typed array to a typed array? - if (canOwn) { - assert(position === 0, 'canOwn must imply no weird position inside the file'); - node.contents = buffer.subarray(offset, offset + length); - node.usedBytes = length; - return length; - } else if (node.usedBytes === 0 && position === 0) { // If this is a simple first write to an empty file, do a fast set since we don't need to care about old data. - node.contents = buffer.slice(offset, offset + length); - node.usedBytes = length; - return length; - } else if (position + length <= node.usedBytes) { // Writing to an already allocated and used subrange of the file? - node.contents.set(buffer.subarray(offset, offset + length), position); - return length; - } - } - - // Appending to an existing file and we need to reallocate, or source data did not come as a typed array. - MEMFS.expandFileStorage(node, position+length); - if (node.contents.subarray && buffer.subarray) { + node.mtime = node.ctime = Date.now(); + + if (canOwn) { + assert(position === 0, 'canOwn must imply no weird position inside the file'); + node.contents = buffer.subarray(offset, offset + length); + node.usedBytes = length; + } else if (node.usedBytes === 0 && position === 0) { // If this is a simple first write to an empty file, do a fast set since we don't need to care about old data. + node.contents = buffer.slice(offset, offset + length); + node.usedBytes = length; + } else { + MEMFS.expandFileStorage(node, position+length); // Use typed array write which is available. node.contents.set(buffer.subarray(offset, offset + length), position); - } else { - for (var i = 0; i < length; i++) { - node.contents[position + i] = buffer[offset + i]; // Or fall back to manual write if not. - } + node.usedBytes = Math.max(node.usedBytes, position + length); } - node.usedBytes = Math.max(node.usedBytes, position + length); return length; }, llseek(stream, offset, whence) { @@ -1855,10 +1623,6 @@ function dbg(text) { } return position; }, - allocate(stream, offset, length) { - MEMFS.expandFileStorage(stream.node, offset + length); - stream.node.usedBytes = Math.max(stream.node.usedBytes, offset + length); - }, mmap(stream, length, position, prot, flags) { if (!FS.isFile(stream.node.mode)) { throw new FS.ErrnoError(43); @@ -1873,20 +1637,22 @@ function dbg(text) { allocated = false; ptr = contents.byteOffset; } else { - // Try to avoid unnecessary slices. - if (position > 0 || position + length < contents.length) { - if (contents.subarray) { - contents = contents.subarray(position, position + length); - } else { - contents = Array.prototype.slice.call(contents, position, position + length); - } - } allocated = true; ptr = mmapAlloc(length); if (!ptr) { throw new FS.ErrnoError(48); } - HEAP8.set(contents, ptr); + if (contents) { + // Try to avoid unnecessary slices. + if (position > 0 || position + length < contents.length) { + if (contents.subarray) { + contents = contents.subarray(position, position + length); + } else { + contents = Array.prototype.slice.call(contents, position, position + length); + } + } + HEAP8.set(contents, ptr); + } } return { ptr, allocated }; }, @@ -1898,74 +1664,8 @@ function dbg(text) { }, }; - /** @param {boolean=} noRunDep */ - var asyncLoad = (url, onload, onerror, noRunDep) => { - var dep = !noRunDep ? getUniqueRunDependency(`al ${url}`) : ''; - readAsync(url, (arrayBuffer) => { - assert(arrayBuffer, `Loading data file "${url}" failed (no arrayBuffer).`); - onload(new Uint8Array(arrayBuffer)); - if (dep) removeRunDependency(dep); - }, (event) => { - if (onerror) { - onerror(); - } else { - throw `Loading data file "${url}" failed.`; - } - }); - if (dep) addRunDependency(dep); - }; - - - var FS_createDataFile = (parent, name, fileData, canRead, canWrite, canOwn) => { - FS.createDataFile(parent, name, fileData, canRead, canWrite, canOwn); - }; - - var preloadPlugins = Module['preloadPlugins'] || []; - var FS_handledByPreloadPlugin = (byteArray, fullname, finish, onerror) => { - // Ensure plugins are ready. - if (typeof Browser != 'undefined') Browser.init(); - - var handled = false; - preloadPlugins.forEach((plugin) => { - if (handled) return; - if (plugin['canHandle'](fullname)) { - plugin['handle'](byteArray, fullname, finish, onerror); - handled = true; - } - }); - return handled; - }; - var FS_createPreloadedFile = (parent, name, url, canRead, canWrite, onload, onerror, dontCreateFile, canOwn, preFinish) => { - // TODO we should allow people to just pass in a complete filename instead - // of parent and name being that we just join them anyways - var fullname = name ? PATH_FS.resolve(PATH.join2(parent, name)) : parent; - var dep = getUniqueRunDependency(`cp ${fullname}`); // might have several active requests for the same fullname - function processData(byteArray) { - function finish(byteArray) { - if (preFinish) preFinish(); - if (!dontCreateFile) { - FS_createDataFile(parent, name, byteArray, canRead, canWrite, canOwn); - } - if (onload) onload(); - removeRunDependency(dep); - } - if (FS_handledByPreloadPlugin(byteArray, fullname, finish, () => { - if (onerror) onerror(); - removeRunDependency(dep); - })) { - return; - } - finish(byteArray); - } - addRunDependency(dep); - if (typeof url == 'string') { - asyncLoad(url, (byteArray) => processData(byteArray), onerror); - } else { - processData(url); - } - }; - var FS_modeStringToFlags = (str) => { + if (typeof str != 'string') return str; var flagModes = { 'r': 0, 'r+': 2, @@ -1981,6 +1681,16 @@ function dbg(text) { return flags; }; + var FS_fileDataToTypedArray = (data) => { + if (typeof data == 'string') { + data = intArrayFromString(data, true); + } + if (!data.subarray) { + data = new Uint8Array(data); + } + return data; + }; + var FS_getMode = (canRead, canWrite) => { var mode = 0; if (canRead) mode |= 292 | 73; @@ -1991,8 +1701,6 @@ function dbg(text) { - - var ERRNO_CODES = { 'EPERM': 63, 'ENOENT': 44, @@ -2121,11 +1829,7 @@ function dbg(text) { isWindows:false, staticInit() { NODEFS.isWindows = !!process.platform.match(/^win/); - var flags = process.binding("constants"); - // Node.js 4 compatibility: it has no namespaces for constants - if (flags["fs"]) { - flags = flags["fs"]; - } + var flags = process.binding("constants")["fs"]; NODEFS.flagsForNodeMap = { "1024": flags["O_APPEND"], "64": flags["O_CREAT"], @@ -2147,6 +1851,17 @@ function dbg(text) { assert(code in ERRNO_CODES, `unexpected node error code: ${code} (${e})`); return ERRNO_CODES[code]; }, + tryFSOperation(f) { + try { + return f(); + } catch (e) { + if (!e.code) throw e; + // node under windows can return code 'UNKNOWN' here: + // https://github.com/emscripten-core/emscripten/issues/15468 + if (e.code === 'UNKNOWN') throw new FS.ErrnoError(28); + throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); + } + }, mount(mount) { assert(ENVIRONMENT_IS_NODE); return NODEFS.createNode(null, '/', NODEFS.getMode(mount.opts.root), 0); @@ -2161,19 +1876,15 @@ function dbg(text) { return node; }, getMode(path) { - var stat; - try { - stat = fs.lstatSync(path); + return NODEFS.tryFSOperation(() => { + var mode = fs.lstatSync(path).mode; if (NODEFS.isWindows) { - // Node.js on Windows never represents permission bit 'x', so - // propagate read bits to execute bits - stat.mode = stat.mode | ((stat.mode & 292) >> 2); + // Windows does not report the 'x' permission bit, so propagate read + // bits to execute bits. + mode |= (mode & 292) >> 2; } - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } - return stat.mode; + return mode; + }); }, realPath(node) { var parts = []; @@ -2183,7 +1894,7 @@ function dbg(text) { } parts.push(node.mount.opts.root); parts.reverse(); - return PATH.join.apply(null, parts); + return PATH.join(...parts); }, flagsForNode(flags) { flags &= ~2097152; // Ignore this flag from musl, otherwise node.js fails to open the file. @@ -2203,59 +1914,77 @@ function dbg(text) { } return newFlags; }, - node_ops:{ - getattr(node) { - var path = NODEFS.realPath(node); - var stat; - try { - stat = fs.lstatSync(path); - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } - // node.js v0.10.20 doesn't report blksize and blocks on Windows. Fake them with default blksize of 4096. + getattr(func, node) { + var stat = NODEFS.tryFSOperation(func); + if (NODEFS.isWindows) { + // node.js v0.10.20 doesn't report blksize and blocks on Windows. Fake + // them with default blksize of 4096. // See http://support.microsoft.com/kb/140365 - if (NODEFS.isWindows && !stat.blksize) { + if (!stat.blksize) { stat.blksize = 4096; } - if (NODEFS.isWindows && !stat.blocks) { + if (!stat.blocks) { stat.blocks = (stat.size+stat.blksize-1)/stat.blksize|0; } - return { - dev: stat.dev, - ino: stat.ino, - mode: stat.mode, - nlink: stat.nlink, - uid: stat.uid, - gid: stat.gid, - rdev: stat.rdev, - size: stat.size, - atime: stat.atime, - mtime: stat.mtime, - ctime: stat.ctime, - blksize: stat.blksize, - blocks: stat.blocks - }; + // Windows does not report the 'x' permission bit, so propagate read + // bits to execute bits. + stat.mode |= (stat.mode & 292) >> 2; + } + return { + dev: stat.dev, + ino: node.id, + mode: stat.mode, + nlink: stat.nlink, + uid: stat.uid, + gid: stat.gid, + rdev: stat.rdev, + size: stat.size, + atime: stat.atime, + mtime: stat.mtime, + ctime: stat.ctime, + blksize: stat.blksize, + blocks: stat.blocks + }; + }, + setattr(arg, node, attr, chmod, utimes, truncate, stat) { + NODEFS.tryFSOperation(() => { + if (attr.mode !== undefined) { + var mode = attr.mode; + if (NODEFS.isWindows) { + // Windows only supports S_IREAD / S_IWRITE (S_IRUSR / S_IWUSR) + // https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/chmod-wchmod + mode &= 384; + } + chmod(arg, mode); + // update the common node structure mode as well + node.mode = attr.mode; + } + if (typeof (attr.atime ?? attr.mtime) === "number") { + // Unfortunately, we have to stat the current value if we don't want + // to change it. On top of that, since the times don't round trip + // this will only keep the value nearly unchanged not exactly + // unchanged. See: + // https://github.com/nodejs/node/issues/56492 + var atime = new Date(attr.atime ?? stat(arg).atime); + var mtime = new Date(attr.mtime ?? stat(arg).mtime); + utimes(arg, atime, mtime); + } + if (attr.size !== undefined) { + truncate(arg, attr.size); + } + }); + }, + node_ops:{ + getattr(node) { + var path = NODEFS.realPath(node); + return NODEFS.getattr(() => fs.lstatSync(path), node); }, setattr(node, attr) { var path = NODEFS.realPath(node); - try { - if (attr.mode !== undefined) { - fs.chmodSync(path, attr.mode); - // update the common node structure mode as well - node.mode = attr.mode; - } - if (attr.timestamp !== undefined) { - var date = new Date(attr.timestamp); - fs.utimesSync(path, date, date); - } - if (attr.size !== undefined) { - fs.truncateSync(path, attr.size); - } - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); + if (attr.mode != null && attr.dontFollow) { + throw new FS.ErrnoError(52); } + NODEFS.setattr(path, node, attr, fs.chmodSync, fs.utimesSync, fs.truncateSync, fs.lstatSync); }, lookup(parent, name) { var path = PATH.join2(NODEFS.realPath(parent), name); @@ -2266,117 +1995,85 @@ function dbg(text) { var node = NODEFS.createNode(parent, name, mode, dev); // create the backing node for this in the fs root as well var path = NODEFS.realPath(node); - try { + NODEFS.tryFSOperation(() => { if (FS.isDir(node.mode)) { fs.mkdirSync(path, node.mode); } else { fs.writeFileSync(path, '', { mode: node.mode }); } - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + }); return node; }, rename(oldNode, newDir, newName) { var oldPath = NODEFS.realPath(oldNode); var newPath = PATH.join2(NODEFS.realPath(newDir), newName); try { - fs.renameSync(oldPath, newPath); - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + FS.unlink(newPath); + } catch(e) {} + NODEFS.tryFSOperation(() => fs.renameSync(oldPath, newPath)); oldNode.name = newName; }, unlink(parent, name) { var path = PATH.join2(NODEFS.realPath(parent), name); - try { - fs.unlinkSync(path); - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + NODEFS.tryFSOperation(() => fs.unlinkSync(path)); }, rmdir(parent, name) { var path = PATH.join2(NODEFS.realPath(parent), name); - try { - fs.rmdirSync(path); - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + NODEFS.tryFSOperation(() => fs.rmdirSync(path)); }, readdir(node) { var path = NODEFS.realPath(node); - try { - return fs.readdirSync(path); - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + return NODEFS.tryFSOperation(() => fs.readdirSync(path)); }, symlink(parent, newName, oldPath) { var newPath = PATH.join2(NODEFS.realPath(parent), newName); - try { - fs.symlinkSync(oldPath, newPath); - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + NODEFS.tryFSOperation(() => fs.symlinkSync(oldPath, newPath)); }, readlink(node) { var path = NODEFS.realPath(node); - try { - path = fs.readlinkSync(path); - path = nodePath.relative(nodePath.resolve(node.mount.opts.root), path); - return path; - } catch (e) { - if (!e.code) throw e; - // node under windows can return code 'UNKNOWN' here: - // https://github.com/emscripten-core/emscripten/issues/15468 - if (e.code === 'UNKNOWN') throw new FS.ErrnoError(28); - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + return NODEFS.tryFSOperation(() => fs.readlinkSync(path)); + }, + statfs(path) { + var stats = NODEFS.tryFSOperation(() => fs.statfsSync(path)); + // Node.js doesn't provide frsize (fragment size). Set it to bsize (block size) + // as they're often the same in many file systems. May not be accurate for all. + stats.frsize = stats.bsize; + return stats; }, }, stream_ops:{ + getattr(stream) { + return NODEFS.getattr(() => fs.fstatSync(stream.nfd), stream.node); + }, + setattr(stream, attr) { + NODEFS.setattr(stream.nfd, stream.node, attr, fs.fchmodSync, fs.futimesSync, fs.ftruncateSync, fs.fstatSync); + }, open(stream) { var path = NODEFS.realPath(stream.node); - try { - if (FS.isFile(stream.node.mode)) { - stream.nfd = fs.openSync(path, NODEFS.flagsForNode(stream.flags)); - } - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + NODEFS.tryFSOperation(() => { + stream.shared.refcount = 1; + stream.nfd = fs.openSync(path, NODEFS.flagsForNode(stream.flags)); + }); }, close(stream) { - try { - if (FS.isFile(stream.node.mode) && stream.nfd) { + NODEFS.tryFSOperation(() => { + if (stream.nfd && --stream.shared.refcount === 0) { fs.closeSync(stream.nfd); } - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + }); + }, + dup(stream) { + stream.shared.refcount++; }, read(stream, buffer, offset, length, position) { - // Node.js < 6 compatibility: node errors on 0 length reads - if (length === 0) return 0; - try { - return fs.readSync(stream.nfd, new Int8Array(buffer.buffer, offset, length), { position: position }); - } catch (e) { - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + return NODEFS.tryFSOperation(() => + fs.readSync(stream.nfd, buffer, offset, length, position) + ); }, write(stream, buffer, offset, length, position) { - try { - return fs.writeSync(stream.nfd, new Int8Array(buffer.buffer, offset, length), { position: position }); - } catch (e) { - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + return NODEFS.tryFSOperation(() => + fs.writeSync(stream.nfd, buffer, offset, length, position) + ); }, llseek(stream, offset, whence) { var position = offset; @@ -2384,12 +2081,10 @@ function dbg(text) { position += stream.position; } else if (whence === 2) { if (FS.isFile(stream.node.mode)) { - try { + NODEFS.tryFSOperation(() => { var stat = fs.fstatSync(stream.nfd); position += stat.size; - } catch (e) { - throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); - } + }); } } @@ -2421,6 +2116,70 @@ function dbg(text) { + + var NODERAWFS_stream_funcs = { + close(stream) { + VFS.closeStream(stream.fd); + // Don't close stdin/stdout/stderr since they are used by node itself. + if (--stream.shared.refcnt <= 0 && stream.nfd > 2) { + // This stream is created by our Node.js filesystem, close the + // native file descriptor when its reference count drops to 0. + fs.closeSync(stream.nfd); + } + }, + llseek(stream, offset, whence) { + var position = offset; + if (whence === 1) { + position += stream.position; + } else if (whence === 2) { + position += fs.fstatSync(stream.nfd).size; + } else if (whence !== 0) { + throw new FS.ErrnoError(28); + } + + if (position < 0) { + throw new FS.ErrnoError(28); + } + stream.position = position; + return position; + }, + read(stream, buffer, offset, length, position) { + var seeking = typeof position != 'undefined'; + if (!seeking && stream.seekable) position = stream.position; + var bytesRead = fs.readSync(stream.nfd, buffer, offset, length, position); + // update position marker when non-seeking + if (!seeking) stream.position += bytesRead; + return bytesRead; + }, + write(stream, buffer, offset, length, position) { + if (stream.flags & 1024) { + // seek to the end before writing in append mode + FS.llseek(stream, 0, 2); + } + var seeking = typeof position != 'undefined'; + if (!seeking && stream.seekable) position = stream.position; + var bytesWritten = fs.writeSync(stream.nfd, buffer, offset, length, position); + // update position marker when non-seeking + if (!seeking) stream.position += bytesWritten; + return bytesWritten; + }, + mmap(stream, length, position, prot, flags) { + if (!length) { + throw new FS.ErrnoError(28); + } + var ptr = mmapAlloc(length); + FS.read(stream, HEAP8, ptr, length, position); + return { ptr, allocated: true }; + }, + msync(stream, buffer, offset, length, mmapFlags) { + FS.write(stream, buffer, 0, length, offset); + // should we check if bytesWritten and length are the same? + return 0; + }, + ioctl(stream, cmd, arg) { + throw new FS.ErrnoError(59); + }, + }; var NODERAWFS = { lookup(parent, name) { assert(parent) @@ -2429,20 +2188,21 @@ function dbg(text) { }, lookupPath(path, opts = {}) { if (opts.parent) { - path = nodePath.dirname(path); + path = PATH.dirname(path); } var st = fs.lstatSync(path); var mode = NODEFS.getMode(path); return { path, node: { id: st.ino, mode, node_ops: NODERAWFS, path }}; }, createStandardStreams() { - FS.createStream({ nfd: 0, position: 0, path: '', flags: 0, tty: true, seekable: false }, 0); + FS.createStream({ nfd: 0, position: 0, path: '/dev/stdin', flags: 0, seekable: false }, 0); + var paths = [,'/dev/stdout', '/dev/stderr']; for (var i = 1; i < 3; i++) { - FS.createStream({ nfd: i, position: 0, path: '', flags: 577, tty: true, seekable: false }, i); + FS.createStream({ nfd: i, position: 0, path: paths[i], flags: 577, seekable: false }, i); } }, cwd() { return process.cwd(); }, - chdir() { process.chdir.apply(void 0, arguments); }, + chdir(...args) { process.chdir(...args); }, mknod(path, mode) { if (FS.isDir(path)) { fs.mkdirSync(path, mode); @@ -2450,26 +2210,69 @@ function dbg(text) { fs.writeFileSync(path, '', { mode: mode }); } }, - mkdir() { fs.mkdirSync.apply(void 0, arguments); }, - symlink() { fs.symlinkSync.apply(void 0, arguments); }, - rename() { fs.renameSync.apply(void 0, arguments); }, - rmdir() { fs.rmdirSync.apply(void 0, arguments); }, - readdir() { return ['.', '..'].concat(fs.readdirSync.apply(void 0, arguments)); }, - unlink() { fs.unlinkSync.apply(void 0, arguments); }, - readlink() { return fs.readlinkSync.apply(void 0, arguments); }, - stat() { return fs.statSync.apply(void 0, arguments); }, - lstat() { return fs.lstatSync.apply(void 0, arguments); }, - chmod() { fs.chmodSync.apply(void 0, arguments); }, + mkdir(...args) { fs.mkdirSync(...args); }, + symlink(...args) { fs.symlinkSync(...args); }, + rename(...args) { fs.renameSync(...args); }, + rmdir(...args) { fs.rmdirSync(...args); }, + readdir(...args) { return ['.', '..'].concat(fs.readdirSync(...args)); }, + unlink(...args) { fs.unlinkSync(...args); }, + readlink(...args) { return fs.readlinkSync(...args); }, + stat(path, dontFollow) { + var stat = dontFollow ? fs.lstatSync(path) : fs.statSync(path); + if (NODEFS.isWindows) { + // Windows does not report the 'x' permission bit, so propagate read + // bits to execute bits. + stat.mode |= (stat.mode & 292) >> 2; + } + return stat; + }, + fstat(fd) { + var stream = FS.getStreamChecked(fd); + return fs.fstatSync(stream.nfd); + }, + statfs(path) { + // Node's fs.statfsSync API doesn't provide these attributes so include + // some defaults. + var defaults = { + fsid: 42, + flags: 2, + namelen: 255, + } + return Object.assign(defaults, fs.statfsSync(path)); + }, + statfsStream(stream) { + return FS.statfs(stream.path); + }, + chmod(path, mode, dontFollow) { + mode &= 4095; + if (NODEFS.isWindows) { + // Windows only supports S_IREAD / S_IWRITE (S_IRUSR / S_IWUSR) + // https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/chmod-wchmod + mode &= 384; + } + if (dontFollow && fs.lstatSync(path).isSymbolicLink()) { + // Node (and indeed linux) does not support chmod on symlinks + // https://nodejs.org/api/fs.html#fslchmodsyncpath-mode + throw new FS.ErrnoError(138); + } + fs.chmodSync(path, mode); + }, fchmod(fd, mode) { var stream = FS.getStreamChecked(fd); fs.fchmodSync(stream.nfd, mode); }, - chown() { fs.chownSync.apply(void 0, arguments); }, + chown(...args) { fs.chownSync(...args); }, fchown(fd, owner, group) { var stream = FS.getStreamChecked(fd); fs.fchownSync(stream.nfd, owner, group); }, - truncate() { fs.truncateSync.apply(void 0, arguments); }, + truncate(path, len) { + // See https://github.com/nodejs/node/issues/35632 + if (len < 0) { + throw new FS.ErrnoError(28); + } + return fs.truncateSync(path, len); + }, ftruncate(fd, len) { // See https://github.com/nodejs/node/issues/35632 if (len < 0) { @@ -2478,12 +2281,20 @@ function dbg(text) { var stream = FS.getStreamChecked(fd); fs.ftruncateSync(stream.nfd, len); }, - utime(path, atime, mtime) { fs.utimesSync(path, atime/1000, mtime/1000); }, - open(path, flags, mode) { - if (typeof flags == "string") { - flags = FS_modeStringToFlags(flags) + utime(path, atime, mtime) { + // null here for atime or mtime means UTIME_OMIT was passed. Since node + // doesn't support this concept we need to first find the existing + // timestamps in order to preserve them. + if ((atime === null) || (mtime === null)) { + var st = fs.statSync(path); + atime ||= st.atimeMs; + mtime ||= st.mtimeMs; } - var pathTruncated = path.split('/').map(function(s) { return s.substr(0, 255); }).join('/'); + fs.utimesSync(path, atime/1000, mtime/1000); + }, + open(path, flags, mode) { + flags = FS_modeStringToFlags(flags); + var pathTruncated = path.split('/').map((s) => s.slice(0, 255)).join('/'); var nfd = fs.openSync(pathTruncated, NODEFS.flagsForNode(flags), mode); var st = fs.fstatSync(nfd); if (flags & 65536 && !st.isDirectory()) { @@ -2497,235 +2308,97 @@ function dbg(text) { createStream(stream, fd) { // Call the original FS.createStream var rtn = VFS.createStream(stream, fd); - if (typeof rtn.shared.refcnt == 'undefined') { - rtn.shared.refcnt = 1; - } else { + // Detect PIPEFS streams and skip the refcnt/tty initialization in that case. + if (!stream.stream_ops) { + rtn.shared.refcnt ??= 0; rtn.shared.refcnt++; + rtn.tty = nodeTTY.isatty(rtn.nfd); } return rtn; }, - close(stream) { - VFS.closeStream(stream.fd); - if (!stream.stream_ops && --stream.shared.refcnt === 0) { - // This stream is created by our Node.js filesystem, close the - // native file descriptor when its reference count drops to 0. - fs.closeSync(stream.nfd); - } - }, - llseek(stream, offset, whence) { - if (stream.stream_ops) { - // this stream is created by in-memory filesystem - return VFS.llseek(stream, offset, whence); - } - var position = offset; - if (whence === 1) { - position += stream.position; - } else if (whence === 2) { - position += fs.fstatSync(stream.nfd).size; - } else if (whence !== 0) { - throw new FS.ErrnoError(28); - } + }; - if (position < 0) { - throw new FS.ErrnoError(28); - } - stream.position = position; - return position; - }, - read(stream, buffer, offset, length, position) { - if (stream.stream_ops) { - // this stream is created by in-memory filesystem - return VFS.read(stream, buffer, offset, length, position); - } - var seeking = typeof position != 'undefined'; - if (!seeking && stream.seekable) position = stream.position; - var bytesRead = fs.readSync(stream.nfd, new Int8Array(buffer.buffer, offset, length), { position: position }); - // update position marker when non-seeking - if (!seeking) stream.position += bytesRead; - return bytesRead; - }, - write(stream, buffer, offset, length, position) { - if (stream.stream_ops) { - // this stream is created by in-memory filesystem - return VFS.write(stream, buffer, offset, length, position); - } - if (stream.flags & +"1024") { - // seek to the end before writing in append mode - FS.llseek(stream, 0, +"2"); - } - var seeking = typeof position != 'undefined'; - if (!seeking && stream.seekable) position = stream.position; - var bytesWritten = fs.writeSync(stream.nfd, new Int8Array(buffer.buffer, offset, length), { position: position }); - // update position marker when non-seeking - if (!seeking) stream.position += bytesWritten; - return bytesWritten; - }, - allocate() { - throw new FS.ErrnoError(138); - }, - mmap(stream, length, position, prot, flags) { - if (stream.stream_ops) { - // this stream is created by in-memory filesystem - return VFS.mmap(stream, length, position, prot, flags); - } - var ptr = mmapAlloc(length); - FS.read(stream, HEAP8, ptr, length, position); - return { ptr, allocated: true }; - }, - msync(stream, buffer, offset, length, mmapFlags) { - if (stream.stream_ops) { - // this stream is created by in-memory filesystem - return VFS.msync(stream, buffer, offset, length, mmapFlags); - } - FS.write(stream, buffer, 0, length, offset); - // should we check if bytesWritten and length are the same? - return 0; - }, - munmap() { - return 0; - }, - ioctl() { - throw new FS.ErrnoError(59); - }, - }; + /** + * Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the + * emscripten HEAP, returns a copy of that string as a Javascript String object. + * + * @param {number} ptr + * @param {number=} maxBytesToRead - An optional length that specifies the + * maximum number of bytes to read. You can omit this parameter to scan the + * string until the first 0 byte. If maxBytesToRead is passed, and the string + * at [ptr, ptr+maxBytesToReadr[ contains a null byte in the middle, then the + * string will cut short at that byte index. + * @param {boolean=} ignoreNul - If true, the function will not stop on a NUL character. + * @return {string} + */ + var UTF8ToString = (ptr, maxBytesToRead, ignoreNul) => { + assert(typeof ptr == 'number', `UTF8ToString expects a number (got ${typeof ptr})`); + return ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead, ignoreNul) : ''; + }; - var ERRNO_MESSAGES = { - 0:"Success", - 1:"Arg list too long", - 2:"Permission denied", - 3:"Address already in use", - 4:"Address not available", - 5:"Address family not supported by protocol family", - 6:"No more processes", - 7:"Socket already connected", - 8:"Bad file number", - 9:"Trying to read unreadable message", - 10:"Mount device busy", - 11:"Operation canceled", - 12:"No children", - 13:"Connection aborted", - 14:"Connection refused", - 15:"Connection reset by peer", - 16:"File locking deadlock error", - 17:"Destination address required", - 18:"Math arg out of domain of func", - 19:"Quota exceeded", - 20:"File exists", - 21:"Bad address", - 22:"File too large", - 23:"Host is unreachable", - 24:"Identifier removed", - 25:"Illegal byte sequence", - 26:"Connection already in progress", - 27:"Interrupted system call", - 28:"Invalid argument", - 29:"I/O error", - 30:"Socket is already connected", - 31:"Is a directory", - 32:"Too many symbolic links", - 33:"Too many open files", - 34:"Too many links", - 35:"Message too long", - 36:"Multihop attempted", - 37:"File or path name too long", - 38:"Network interface is not configured", - 39:"Connection reset by network", - 40:"Network is unreachable", - 41:"Too many open files in system", - 42:"No buffer space available", - 43:"No such device", - 44:"No such file or directory", - 45:"Exec format error", - 46:"No record locks available", - 47:"The link has been severed", - 48:"Not enough core", - 49:"No message of desired type", - 50:"Protocol not available", - 51:"No space left on device", - 52:"Function not implemented", - 53:"Socket is not connected", - 54:"Not a directory", - 55:"Directory not empty", - 56:"State not recoverable", - 57:"Socket operation on non-socket", - 59:"Not a typewriter", - 60:"No such device or address", - 61:"Value too large for defined data type", - 62:"Previous owner died", - 63:"Not super-user", - 64:"Broken pipe", - 65:"Protocol error", - 66:"Unknown protocol", - 67:"Protocol wrong type for socket", - 68:"Math result not representable", - 69:"Read only file system", - 70:"Illegal seek", - 71:"No such process", - 72:"Stale file handle", - 73:"Connection timed out", - 74:"Text file busy", - 75:"Cross-device link", - 100:"Device not a stream", - 101:"Bad font file fmt", - 102:"Invalid slot", - 103:"Invalid request code", - 104:"No anode", - 105:"Block device required", - 106:"Channel number out of range", - 107:"Level 3 halted", - 108:"Level 3 reset", - 109:"Link number out of range", - 110:"Protocol driver not attached", - 111:"No CSI structure available", - 112:"Level 2 halted", - 113:"Invalid exchange", - 114:"Invalid request descriptor", - 115:"Exchange full", - 116:"No data (for no delay io)", - 117:"Timer expired", - 118:"Out of streams resources", - 119:"Machine is not on the network", - 120:"Package not installed", - 121:"The object is remote", - 122:"Advertise error", - 123:"Srmount error", - 124:"Communication error on send", - 125:"Cross mount point (not really error)", - 126:"Given log. name not unique", - 127:"f.d. invalid for this operation", - 128:"Remote address changed", - 129:"Can access a needed shared lib", - 130:"Accessing a corrupted shared lib", - 131:".lib section in a.out corrupted", - 132:"Attempting to link in too many libs", - 133:"Attempting to exec a shared library", - 135:"Streams pipe error", - 136:"Too many users", - 137:"Socket type not supported", - 138:"Not supported", - 139:"Protocol family not supported", - 140:"Can't send after socket shutdown", - 141:"Too many references", - 142:"Host is down", - 148:"No medium (in tape drive)", - 156:"Level 2 not synchronized", - }; + var strError = (errno) => UTF8ToString(_strerror(errno)); - var demangle = (func) => { - warnOnce('warning: build with -sDEMANGLE_SUPPORT to link in libcxxabi demangling'); - return func; + var asyncLoad = async (url) => { + var arrayBuffer = await readAsync(url); + assert(arrayBuffer, `Loading data file "${url}" failed (no arrayBuffer).`); + return new Uint8Array(arrayBuffer); }; - var demangleAll = (text) => { - var regex = - /\b_Z[\w\d_]+/g; - return text.replace(regex, - function(x) { - var y = demangle(x); - return x === y ? x : (y + ' [' + x + ']'); - }); + + + var FS_createDataFile = (...args) => FS.createDataFile(...args); + + var getUniqueRunDependency = (id) => { + var orig = id; + while (1) { + if (!runDependencyTracking[id]) return id; + id = orig + Math.random(); + } + }; + + + + var preloadPlugins = []; + var FS_handledByPreloadPlugin = async (byteArray, fullname) => { + // Ensure plugins are ready. + if (typeof Browser != 'undefined') Browser.init(); + + for (var plugin of preloadPlugins) { + if (plugin['canHandle'](fullname)) { + assert(plugin['handle'].constructor.name === 'AsyncFunction', 'Filesystem plugin handlers must be async functions (See #24914)') + return plugin['handle'](byteArray, fullname); + } + } + // If no plugin handled this file then return the original/unmodified + // byteArray. + return byteArray; + }; + var FS_preloadFile = async (parent, name, url, canRead, canWrite, dontCreateFile, canOwn, preFinish) => { + // TODO we should allow people to just pass in a complete filename instead + // of parent and name being that we just join them anyways + var fullname = name ? PATH_FS.resolve(PATH.join2(parent, name)) : parent; + var dep = getUniqueRunDependency(`cp ${fullname}`); // might have several active requests for the same fullname + addRunDependency(dep); + + try { + var byteArray = url; + if (typeof url == 'string') { + byteArray = await asyncLoad(url); + } + + byteArray = await FS_handledByPreloadPlugin(byteArray, fullname); + preFinish?.(); + if (!dontCreateFile) { + FS_createDataFile(parent, name, byteArray, canRead, canWrite, canOwn); + } + } finally { + removeRunDependency(dep); + } + }; + var FS_createPreloadedFile = (parent, name, url, canRead, canWrite, onload, onerror, dontCreateFile, canOwn, preFinish) => { + FS_preloadFile(parent, name, url, canRead, canWrite, dontCreateFile, canOwn, preFinish).then(onload).catch(onerror); }; var FS = { root:null, @@ -2738,69 +2411,173 @@ function dbg(text) { currentPath:"/", initialized:false, ignorePermissions:true, - ErrnoError:null, - genericErrors:{ - }, filesystems:null, syncFSRequests:0, + ErrnoError:class extends Error { + name = 'ErrnoError'; + // We set the `name` property to be able to identify `FS.ErrnoError` + // - the `name` is a standard ECMA-262 property of error objects. Kind of good to have it anyway. + // - when using PROXYFS, an error can come from an underlying FS + // as different FS objects have their own FS.ErrnoError each, + // the test `err instanceof FS.ErrnoError` won't detect an error coming from another filesystem, causing bugs. + // we'll use the reliable test `err.name == "ErrnoError"` instead + constructor(errno) { + super(runtimeInitialized ? strError(errno) : ''); + this.errno = errno; + for (var key in ERRNO_CODES) { + if (ERRNO_CODES[key] === errno) { + this.code = key; + break; + } + } + } + }, + FSStream:class { + shared = {}; + get object() { + return this.node; + } + set object(val) { + this.node = val; + } + get isRead() { + return (this.flags & 2097155) !== 1; + } + get isWrite() { + return (this.flags & 2097155) !== 0; + } + get isAppend() { + return (this.flags & 1024); + } + get flags() { + return this.shared.flags; + } + set flags(val) { + this.shared.flags = val; + } + get position() { + return this.shared.position; + } + set position(val) { + this.shared.position = val; + } + }, + FSNode:class { + node_ops = {}; + stream_ops = {}; + readMode = 292 | 73; + writeMode = 146; + mounted = null; + constructor(parent, name, mode, rdev) { + if (!parent) { + parent = this; // root node sets parent to itself + } + this.parent = parent; + this.mount = parent.mount; + this.id = FS.nextInode++; + this.name = name; + this.mode = mode; + this.rdev = rdev; + this.atime = this.mtime = this.ctime = Date.now(); + } + get read() { + return (this.mode & this.readMode) === this.readMode; + } + set read(val) { + val ? this.mode |= this.readMode : this.mode &= ~this.readMode; + } + get write() { + return (this.mode & this.writeMode) === this.writeMode; + } + set write(val) { + val ? this.mode |= this.writeMode : this.mode &= ~this.writeMode; + } + get isFolder() { + return FS.isDir(this.mode); + } + get isDevice() { + return FS.isChrdev(this.mode); + } + }, lookupPath(path, opts = {}) { - path = PATH_FS.resolve(path); - - if (!path) return { path: '', node: null }; - - var defaults = { - follow_mount: true, - recurse_count: 0 - }; - opts = Object.assign(defaults, opts) + if (!path) { + throw new FS.ErrnoError(44); + } + opts.follow_mount ??= true - if (opts.recurse_count > 8) { // max recursive lookup of 8 - throw new FS.ErrnoError(32); + if (!PATH.isAbs(path)) { + path = FS.cwd() + '/' + path; } - // split the absolute path - var parts = path.split('/').filter((p) => !!p); + // limit max consecutive symlinks to SYMLOOP_MAX. + linkloop: for (var nlinks = 0; nlinks < 40; nlinks++) { + // split the absolute path + var parts = path.split('/').filter((p) => !!p); - // start at the root - var current = FS.root; - var current_path = '/'; + // start at the root + var current = FS.root; + var current_path = '/'; - for (var i = 0; i < parts.length; i++) { - var islast = (i === parts.length-1); - if (islast && opts.parent) { - // stop resolving - break; - } + for (var i = 0; i < parts.length; i++) { + var islast = (i === parts.length-1); + if (islast && opts.parent) { + // stop resolving + break; + } - current = FS.lookupNode(current, parts[i]); - current_path = PATH.join2(current_path, parts[i]); + if (parts[i] === '.') { + continue; + } - // jump to the mount's root node if this is a mountpoint - if (FS.isMountpoint(current)) { - if (!islast || (islast && opts.follow_mount)) { - current = current.mounted.root; + if (parts[i] === '..') { + current_path = PATH.dirname(current_path); + if (FS.isRoot(current)) { + path = current_path + '/' + parts.slice(i + 1).join('/'); + // We're making progress here, don't let many consecutive ..'s + // lead to ELOOP + nlinks--; + continue linkloop; + } else { + current = current.parent; + } + continue; } - } - // by default, lookupPath will not follow a symlink if it is the final path component. - // setting opts.follow = true will override this behavior. - if (!islast || opts.follow) { - var count = 0; - while (FS.isLink(current.mode)) { - var link = FS.readlink(current_path); - current_path = PATH_FS.resolve(PATH.dirname(current_path), link); + current_path = PATH.join2(current_path, parts[i]); + try { + current = FS.lookupNode(current, parts[i]); + } catch (e) { + // if noent_okay is true, suppress a ENOENT in the last component + // and return an object with an undefined node. This is needed for + // resolving symlinks in the path when creating a file. + if ((e?.errno === 44) && islast && opts.noent_okay) { + return { path: current_path }; + } + throw e; + } - var lookup = FS.lookupPath(current_path, { recurse_count: opts.recurse_count + 1 }); - current = lookup.node; + // jump to the mount's root node if this is a mountpoint + if (FS.isMountpoint(current) && (!islast || opts.follow_mount)) { + current = current.mounted.root; + } - if (count++ > 40) { // limit max consecutive symlinks to 40 (SYMLOOP_MAX). - throw new FS.ErrnoError(32); + // by default, lookupPath will not follow a symlink if it is the final path component. + // setting opts.follow = true will override this behavior. + if (FS.isLink(current.mode) && (!islast || opts.follow)) { + if (!current.node_ops.readlink) { + throw new FS.ErrnoError(52); } + var link = current.node_ops.readlink(current); + if (!PATH.isAbs(link)) { + link = PATH.dirname(current_path) + '/' + link; + } + path = link + '/' + parts.slice(i + 1).join('/'); + continue linkloop; } } + return { path: current_path, node: current }; } - - return { path: current_path, node: current }; + throw new FS.ErrnoError(32); }, getPath(node) { var path; @@ -2845,7 +2622,7 @@ function dbg(text) { lookupNode(parent, name) { var errCode = FS.mayLookup(parent); if (errCode) { - throw new FS.ErrnoError(errCode, parent); + throw new FS.ErrnoError(errCode); } var hash = FS.hashName(parent.id, name); for (var node = FS.nameTable[hash]; node; node = node.name_next) { @@ -2909,20 +2686,26 @@ function dbg(text) { // return 0 if any user, group or owner bits are set. if (perms.includes('r') && !(node.mode & 292)) { return 2; - } else if (perms.includes('w') && !(node.mode & 146)) { + } + if (perms.includes('w') && !(node.mode & 146)) { return 2; - } else if (perms.includes('x') && !(node.mode & 73)) { + } + if (perms.includes('x') && !(node.mode & 73)) { return 2; } return 0; }, mayLookup(dir) { + if (!FS.isDir(dir.mode)) return 54; var errCode = FS.nodePermissions(dir, 'x'); if (errCode) return errCode; if (!dir.node_ops.lookup) return 2; return 0; }, mayCreate(dir, name) { + if (!FS.isDir(dir.mode)) { + return 54; + } try { var node = FS.lookupNode(dir, name); return 20; @@ -2948,10 +2731,8 @@ function dbg(text) { if (FS.isRoot(node) || FS.getPath(node) === FS.cwd()) { return 10; } - } else { - if (FS.isDir(node.mode)) { - return 31; - } + } else if (FS.isDir(node.mode)) { + return 31; } return 0; }, @@ -2961,13 +2742,22 @@ function dbg(text) { } if (FS.isLink(node.mode)) { return 32; - } else if (FS.isDir(node.mode)) { - if (FS.flagsToPermissionString(flags) !== 'r' || // opening for write - (flags & 512)) { // TODO: check for O_SEARCH? (== search for dir only) + } + var mode = FS.flagsToPermissionString(flags); + if (FS.isDir(node.mode)) { + // opening for write + // TODO: check for O_SEARCH? (== search for dir only) + if (mode !== 'r' || (flags & (512 | 64))) { return 31; } } - return FS.nodePermissions(node, FS.flagsToPermissionString(flags)); + return FS.nodePermissions(node, mode); + }, + checkOpExists(op, err) { + if (!op) { + throw new FS.ErrnoError(err); + } + return op; }, MAX_OPEN_FDS:4096, nextfd() { @@ -2987,44 +2777,8 @@ function dbg(text) { }, getStream:(fd) => FS.streams[fd], createStream(stream, fd = -1) { - if (!FS.FSStream) { - FS.FSStream = /** @constructor */ function() { - this.shared = { }; - }; - FS.FSStream.prototype = {}; - Object.defineProperties(FS.FSStream.prototype, { - object: { - /** @this {FS.FSStream} */ - get() { return this.node; }, - /** @this {FS.FSStream} */ - set(val) { this.node = val; } - }, - isRead: { - /** @this {FS.FSStream} */ - get() { return (this.flags & 2097155) !== 1; } - }, - isWrite: { - /** @this {FS.FSStream} */ - get() { return (this.flags & 2097155) !== 0; } - }, - isAppend: { - /** @this {FS.FSStream} */ - get() { return (this.flags & 1024); } - }, - flags: { - /** @this {FS.FSStream} */ - get() { return this.shared.flags; }, - /** @this {FS.FSStream} */ - set(val) { this.shared.flags = val; }, - }, - position : { - /** @this {FS.FSStream} */ - get() { return this.shared.position; }, - /** @this {FS.FSStream} */ - set(val) { this.shared.position = val; }, - }, - }); - } + assert(fd >= -1); + // clone it, so we can return an instance of FSStream stream = Object.assign(new FS.FSStream(), stream); if (fd == -1) { @@ -3037,15 +2791,32 @@ function dbg(text) { closeStream(fd) { FS.streams[fd] = null; }, + dupStream(origStream, fd = -1) { + var stream = FS.createStream(origStream, fd); + stream.stream_ops?.dup?.(stream); + return stream; + }, + doSetAttr(stream, node, attr) { + var setattr = stream?.stream_ops.setattr; + var arg = setattr ? stream : node; + setattr ??= node.node_ops.setattr; + FS.checkOpExists(setattr, 63) + try { + setattr(arg, attr); + } catch (e) { + if (e instanceof RangeError) { + throw new FS.ErrnoError(22); + } + throw e; + } + }, chrdev_stream_ops:{ open(stream) { var device = FS.getDevice(stream.node.rdev); // override node's stream ops with the device's stream.stream_ops = device.stream_ops; // forward the open call - if (stream.stream_ops.open) { - stream.stream_ops.open(stream); - } + stream.stream_ops.open?.(stream); }, llseek() { throw new FS.ErrnoError(70); @@ -3067,7 +2838,7 @@ function dbg(text) { mounts.push(m); - check.push.apply(check, m.mounts); + check.push(...m.mounts); } return mounts; @@ -3107,12 +2878,13 @@ function dbg(text) { }; // sync all mounts - mounts.forEach((mount) => { - if (!mount.type.syncfs) { - return done(null); + for (var mount of mounts) { + if (mount.type.syncfs) { + mount.type.syncfs(mount, populate, done); + } else { + done(null); } - mount.type.syncfs(mount, populate, done); - }); + } }, mount(type, opts, mountpoint) { if (typeof type == 'string') { @@ -3179,9 +2951,7 @@ function dbg(text) { var mount = node.mounted; var mounts = FS.getMounts(mount); - Object.keys(FS.nameTable).forEach((hash) => { - var current = FS.nameTable[hash]; - + for (var [hash, current] of Object.entries(FS.nameTable)) { while (current) { var next = current.name_next; @@ -3191,7 +2961,7 @@ function dbg(text) { current = next; } - }); + } // no longer a mountpoint node.mounted = null; @@ -3208,9 +2978,12 @@ function dbg(text) { var lookup = FS.lookupPath(path, { parent: true }); var parent = lookup.node; var name = PATH.basename(path); - if (!name || name === '.' || name === '..') { + if (!name) { throw new FS.ErrnoError(28); } + if (name === '.' || name === '..') { + throw new FS.ErrnoError(20); + } var errCode = FS.mayCreate(parent, name); if (errCode) { throw new FS.ErrnoError(errCode); @@ -3220,14 +2993,43 @@ function dbg(text) { } return parent.node_ops.mknod(parent, name, mode, dev); }, - create(path, mode) { - mode = mode !== undefined ? mode : 438 /* 0666 */; + statfs(path) { + return FS.statfsNode(FS.lookupPath(path, {follow: true}).node); + }, + statfsStream(stream) { + // We keep a separate statfsStream function because noderawfs overrides + // it. In noderawfs, stream.node is sometimes null. Instead, we need to + // look at stream.path. + return FS.statfsNode(stream.node); + }, + statfsNode(node) { + // NOTE: None of the defaults here are true. We're just returning safe and + // sane values. Currently nodefs and rawfs replace these defaults, + // other file systems leave them alone. + var rtn = { + bsize: 4096, + frsize: 4096, + blocks: 1e6, + bfree: 5e5, + bavail: 5e5, + files: FS.nextInode, + ffree: FS.nextInode - 1, + fsid: 42, + flags: 2, + namelen: 255, + }; + + if (node.node_ops.statfs) { + Object.assign(rtn, node.node_ops.statfs(node.mount.opts.root)); + } + return rtn; + }, + create(path, mode = 0o666) { mode &= 4095; mode |= 32768; return FS.mknod(path, mode, 0); }, - mkdir(path, mode) { - mode = mode !== undefined ? mode : 511 /* 0777 */; + mkdir(path, mode = 0o777) { mode &= 511 | 512; mode |= 16384; return FS.mknod(path, mode, 0); @@ -3235,9 +3037,10 @@ function dbg(text) { mkdirTree(path, mode) { var dirs = path.split('/'); var d = ''; - for (var i = 0; i < dirs.length; ++i) { - if (!dirs[i]) continue; - d += '/' + dirs[i]; + for (var dir of dirs) { + if (!dir) continue; + if (d || PATH.isAbs(path)) d += '/'; + d += dir; try { FS.mkdir(d, mode); } catch(e) { @@ -3248,7 +3051,7 @@ function dbg(text) { mkdev(path, mode, dev) { if (typeof dev == 'undefined') { dev = mode; - mode = 438 /* 0666 */; + mode = 0o666; } mode |= 8192; return FS.mknod(path, mode, dev); @@ -3280,7 +3083,7 @@ function dbg(text) { // parents must exist var lookup, old_dir, new_dir; - // let the errors from non existant directories percolate up + // let the errors from non existent directories percolate up lookup = FS.lookupPath(old_path, { parent: true }); old_dir = lookup.node; lookup = FS.lookupPath(new_path, { parent: true }); @@ -3346,6 +3149,9 @@ function dbg(text) { // do the underlying fs rename try { old_dir.node_ops.rename(old_node, new_dir, new_name); + // update old node (we do this here to avoid each backend + // needing to) + old_node.parent = new_dir; } catch (e) { throw e; } finally { @@ -3375,10 +3181,8 @@ function dbg(text) { readdir(path) { var lookup = FS.lookupPath(path, { follow: true }); var node = lookup.node; - if (!node.node_ops.readdir) { - throw new FS.ErrnoError(54); - } - return node.node_ops.readdir(node); + var readdir = FS.checkOpExists(node.node_ops.readdir, 54); + return readdir(node); }, unlink(path) { var lookup = FS.lookupPath(path, { parent: true }); @@ -3413,22 +3217,33 @@ function dbg(text) { if (!link.node_ops.readlink) { throw new FS.ErrnoError(28); } - return PATH_FS.resolve(FS.getPath(link.parent), link.node_ops.readlink(link)); + return link.node_ops.readlink(link); }, stat(path, dontFollow) { var lookup = FS.lookupPath(path, { follow: !dontFollow }); var node = lookup.node; - if (!node) { - throw new FS.ErrnoError(44); - } - if (!node.node_ops.getattr) { - throw new FS.ErrnoError(63); - } - return node.node_ops.getattr(node); + var getattr = FS.checkOpExists(node.node_ops.getattr, 63); + return getattr(node); + }, + fstat(fd) { + var stream = FS.getStreamChecked(fd); + var node = stream.node; + var getattr = stream.stream_ops.getattr; + var arg = getattr ? stream : node; + getattr ??= node.node_ops.getattr; + FS.checkOpExists(getattr, 63) + return getattr(arg); }, lstat(path) { return FS.stat(path, true); }, + doChmod(stream, node, mode, dontFollow) { + FS.doSetAttr(stream, node, { + mode: (mode & 4095) | (node.mode & ~4095), + ctime: Date.now(), + dontFollow + }); + }, chmod(path, mode, dontFollow) { var node; if (typeof path == 'string') { @@ -3437,20 +3252,21 @@ function dbg(text) { } else { node = path; } - if (!node.node_ops.setattr) { - throw new FS.ErrnoError(63); - } - node.node_ops.setattr(node, { - mode: (mode & 4095) | (node.mode & ~4095), - timestamp: Date.now() - }); + FS.doChmod(null, node, mode, dontFollow); }, lchmod(path, mode) { FS.chmod(path, mode, true); }, fchmod(fd, mode) { var stream = FS.getStreamChecked(fd); - FS.chmod(stream.node, mode); + FS.doChmod(stream, stream.node, mode, false); + }, + doChown(stream, node, dontFollow) { + FS.doSetAttr(stream, node, { + timestamp: Date.now(), + dontFollow + // we ignore the uid / gid for now + }); }, chown(path, uid, gid, dontFollow) { var node; @@ -3460,35 +3276,16 @@ function dbg(text) { } else { node = path; } - if (!node.node_ops.setattr) { - throw new FS.ErrnoError(63); - } - node.node_ops.setattr(node, { - timestamp: Date.now() - // we ignore the uid / gid for now - }); + FS.doChown(null, node, dontFollow); }, lchown(path, uid, gid) { FS.chown(path, uid, gid, true); }, fchown(fd, uid, gid) { var stream = FS.getStreamChecked(fd); - FS.chown(stream.node, uid, gid); + FS.doChown(stream, stream.node, false); }, - truncate(path, len) { - if (len < 0) { - throw new FS.ErrnoError(28); - } - var node; - if (typeof path == 'string') { - var lookup = FS.lookupPath(path, { follow: true }); - node = lookup.node; - } else { - node = path; - } - if (!node.node_ops.setattr) { - throw new FS.ErrnoError(63); - } + doTruncate(stream, node, len) { if (FS.isDir(node.mode)) { throw new FS.ErrnoError(31); } @@ -3499,49 +3296,65 @@ function dbg(text) { if (errCode) { throw new FS.ErrnoError(errCode); } - node.node_ops.setattr(node, { + FS.doSetAttr(stream, node, { size: len, timestamp: Date.now() }); }, + truncate(path, len) { + if (len < 0) { + throw new FS.ErrnoError(28); + } + var node; + if (typeof path == 'string') { + var lookup = FS.lookupPath(path, { follow: true }); + node = lookup.node; + } else { + node = path; + } + FS.doTruncate(null, node, len); + }, ftruncate(fd, len) { var stream = FS.getStreamChecked(fd); - if ((stream.flags & 2097155) === 0) { + if (len < 0 || (stream.flags & 2097155) === 0) { throw new FS.ErrnoError(28); } - FS.truncate(stream.node, len); + FS.doTruncate(stream, stream.node, len); }, utime(path, atime, mtime) { var lookup = FS.lookupPath(path, { follow: true }); var node = lookup.node; - node.node_ops.setattr(node, { - timestamp: Math.max(atime, mtime) + var setattr = FS.checkOpExists(node.node_ops.setattr, 63); + setattr(node, { + atime: atime, + mtime: mtime }); }, - open(path, flags, mode) { + open(path, flags, mode = 0o666) { if (path === "") { throw new FS.ErrnoError(44); } - flags = typeof flags == 'string' ? FS_modeStringToFlags(flags) : flags; - mode = typeof mode == 'undefined' ? 438 /* 0666 */ : mode; + flags = FS_modeStringToFlags(flags); if ((flags & 64)) { mode = (mode & 4095) | 32768; } else { mode = 0; } var node; + var isDirPath; if (typeof path == 'object') { node = path; } else { - path = PATH.normalize(path); - try { - var lookup = FS.lookupPath(path, { - follow: !(flags & 131072) - }); - node = lookup.node; - } catch (e) { - // ignore - } + isDirPath = path.endsWith("/"); + // noent_okay makes it so that if the final component of the path + // doesn't exist, lookupPath returns `node: undefined`. `path` will be + // updated to point to the target of all symlinks. + var lookup = FS.lookupPath(path, { + follow: !(flags & 131072), + noent_okay: true + }); + node = lookup.node; + path = lookup.path; } // perhaps we need to create the node var created = false; @@ -3551,9 +3364,14 @@ function dbg(text) { if ((flags & 128)) { throw new FS.ErrnoError(20); } + } else if (isDirPath) { + throw new FS.ErrnoError(31); } else { // node doesn't exist, try to create it - node = FS.mknod(path, mode, 0); + // Ignore the permission bits here to ensure we can `open` this new + // file below. We use chmod below to apply the permissions once the + // file is open. + node = FS.mknod(path, mode | 0o777, 0); created = true; } } @@ -3600,11 +3418,8 @@ function dbg(text) { if (stream.stream_ops.open) { stream.stream_ops.open(stream); } - if (Module['logReadFiles'] && !(flags & 1)) { - if (!FS.readFiles) FS.readFiles = {}; - if (!(path in FS.readFiles)) { - FS.readFiles[path] = 1; - } + if (created) { + FS.chmod(node, mode & 0o777); } return stream; }, @@ -3670,6 +3485,7 @@ function dbg(text) { }, write(stream, buffer, offset, length, position, canOwn) { assert(offset >= 0); + assert(buffer.subarray, 'FS.write expects a TypedArray'); if (length < 0 || position < 0) { throw new FS.ErrnoError(28); } @@ -3699,24 +3515,6 @@ function dbg(text) { if (!seeking) stream.position += bytesWritten; return bytesWritten; }, - allocate(stream, offset, length) { - if (FS.isClosed(stream)) { - throw new FS.ErrnoError(8); - } - if (offset < 0 || length <= 0) { - throw new FS.ErrnoError(28); - } - if ((stream.flags & 2097155) === 0) { - throw new FS.ErrnoError(8); - } - if (!FS.isFile(stream.node.mode) && !FS.isDir(stream.node.mode)) { - throw new FS.ErrnoError(43); - } - if (!stream.stream_ops.allocate) { - throw new FS.ErrnoError(138); - } - stream.stream_ops.allocate(stream, offset, length); - }, mmap(stream, length, position, prot, flags) { // User requests writing to file (prot & PROT_WRITE != 0). // Checking if we have permissions to write to the file unless @@ -3735,6 +3533,9 @@ function dbg(text) { if (!stream.stream_ops.mmap) { throw new FS.ErrnoError(43); } + if (!length) { + throw new FS.ErrnoError(28); + } return stream.stream_ops.mmap(stream, length, position, prot, flags); }, msync(stream, buffer, offset, length, mmapFlags) { @@ -3744,7 +3545,6 @@ function dbg(text) { } return stream.stream_ops.msync(stream, buffer, offset, length, mmapFlags); }, - munmap:(stream) => 0, ioctl(stream, cmd, arg) { if (!stream.stream_ops.ioctl) { throw new FS.ErrnoError(59); @@ -3755,34 +3555,24 @@ function dbg(text) { opts.flags = opts.flags || 0; opts.encoding = opts.encoding || 'binary'; if (opts.encoding !== 'utf8' && opts.encoding !== 'binary') { - throw new Error(`Invalid encoding type "${opts.encoding}"`); + abort(`Invalid encoding type "${opts.encoding}"`); } - var ret; var stream = FS.open(path, opts.flags); var stat = FS.stat(path); var length = stat.size; var buf = new Uint8Array(length); FS.read(stream, buf, 0, length, 0); if (opts.encoding === 'utf8') { - ret = UTF8ArrayToString(buf, 0); - } else if (opts.encoding === 'binary') { - ret = buf; + buf = UTF8ArrayToString(buf); } FS.close(stream); - return ret; + return buf; }, writeFile(path, data, opts = {}) { opts.flags = opts.flags || 577; var stream = FS.open(path, opts.flags, opts.mode); - if (typeof data == 'string') { - var buf = new Uint8Array(lengthBytesUTF8(data)+1); - var actualNumBytes = stringToUTF8Array(data, buf, 0, buf.length); - FS.write(stream, buf, 0, actualNumBytes, undefined, opts.canOwn); - } else if (ArrayBuffer.isView(data)) { - FS.write(stream, data, 0, data.byteLength, undefined, opts.canOwn); - } else { - throw new Error('Unsupported data type'); - } + data = FS_fileDataToTypedArray(data); + FS.write(stream, data, 0, data.byteLength, undefined, opts.canOwn); FS.close(stream); }, cwd:() => FS.currentPath, @@ -3812,6 +3602,7 @@ function dbg(text) { FS.registerDevice(FS.makedev(1, 3), { read: () => 0, write: (stream, buffer, offset, length, pos) => length, + llseek: () => 0, }); FS.mkdev('/dev/null', FS.makedev(1, 3)); // setup /dev/tty and /dev/tty1 @@ -3826,7 +3617,8 @@ function dbg(text) { var randomBuffer = new Uint8Array(1024), randomLeft = 0; var randomByte = () => { if (randomLeft === 0) { - randomLeft = randomFill(randomBuffer).byteLength; + randomFill(randomBuffer); + randomLeft = randomBuffer.byteLength; } return randomBuffer[--randomLeft]; }; @@ -3845,7 +3637,10 @@ function dbg(text) { FS.mkdir('/proc/self/fd'); FS.mount({ mount() { - var node = FS.createNode(proc_self, 'fd', 16384 | 511 /* 0777 */, 73); + var node = FS.createNode(proc_self, 'fd', 16895, 73); + node.stream_ops = { + llseek: MEMFS.stream_ops.llseek, + }; node.node_ops = { lookup(parent, name) { var fd = +name; @@ -3854,16 +3649,22 @@ function dbg(text) { parent: null, mount: { mountpoint: 'fake' }, node_ops: { readlink: () => stream.path }, + id: fd + 1, }; ret.parent = ret; // make it look like a simple root node return ret; + }, + readdir() { + return Array.from(FS.streams.entries()) + .filter(([k, v]) => v) + .map(([k, v]) => k.toString()); } }; return node; } }, {}, '/proc/self/fd'); }, - createStandardStreams() { + createStandardStreams(input, output, error) { // TODO deprecate the old functionality of a single // input / output callback and that utilizes FS.createDevice // and instead require a unique set of stream ops @@ -3872,18 +3673,18 @@ function dbg(text) { // default tty devices. however, if the standard streams // have been overwritten we create a unique device for // them instead. - if (Module['stdin']) { - FS.createDevice('/dev', 'stdin', Module['stdin']); + if (input) { + FS.createDevice('/dev', 'stdin', input); } else { FS.symlink('/dev/tty', '/dev/stdin'); } - if (Module['stdout']) { - FS.createDevice('/dev', 'stdout', null, Module['stdout']); + if (output) { + FS.createDevice('/dev', 'stdout', null, output); } else { FS.symlink('/dev/tty', '/dev/stdout'); } - if (Module['stderr']) { - FS.createDevice('/dev', 'stderr', null, Module['stderr']); + if (error) { + FS.createDevice('/dev', 'stderr', null, error); } else { FS.symlink('/dev/tty1', '/dev/stderr'); } @@ -3896,48 +3697,7 @@ function dbg(text) { assert(stdout.fd === 1, `invalid handle for stdout (${stdout.fd})`); assert(stderr.fd === 2, `invalid handle for stderr (${stderr.fd})`); }, - ensureErrnoError() { - if (FS.ErrnoError) return; - FS.ErrnoError = /** @this{Object} */ function ErrnoError(errno, node) { - // We set the `name` property to be able to identify `FS.ErrnoError` - // - the `name` is a standard ECMA-262 property of error objects. Kind of good to have it anyway. - // - when using PROXYFS, an error can come from an underlying FS - // as different FS objects have their own FS.ErrnoError each, - // the test `err instanceof FS.ErrnoError` won't detect an error coming from another filesystem, causing bugs. - // we'll use the reliable test `err.name == "ErrnoError"` instead - this.name = 'ErrnoError'; - this.node = node; - this.setErrno = /** @this{Object} */ function(errno) { - this.errno = errno; - for (var key in ERRNO_CODES) { - if (ERRNO_CODES[key] === errno) { - this.code = key; - break; - } - } - }; - this.setErrno(errno); - this.message = ERRNO_MESSAGES[errno]; - - // Try to get a maximally helpful stack trace. On Node.js, getting Error.stack - // now ensures it shows what we want. - if (this.stack) { - // Define the stack property for Node.js 4, which otherwise errors on the next line. - Object.defineProperty(this, "stack", { value: (new Error).stack, writable: true }); - this.stack = demangleAll(this.stack); - } - }; - FS.ErrnoError.prototype = new Error(); - FS.ErrnoError.prototype.constructor = FS.ErrnoError; - // Some errors may happen quite a bit, to avoid overhead we reuse them (and suffer a lack of stack info) - [44].forEach((code) => { - FS.genericErrors[code] = new FS.ErrnoError(code); - FS.genericErrors[code].stack = ''; - }); - }, staticInit() { - FS.ensureErrnoError(); - FS.nameTable = new Array(4096); FS.mount(MEMFS, {}, '/'); @@ -3952,29 +3712,25 @@ function dbg(text) { }; }, init(input, output, error) { - assert(!FS.init.initialized, 'FS.init was previously called. If you want to initialize later with custom parameters, remove any earlier calls (note that one is automatically added to the generated code)'); - FS.init.initialized = true; - - FS.ensureErrnoError(); + assert(!FS.initialized, 'FS.init was previously called. If you want to initialize later with custom parameters, remove any earlier calls (note that one is automatically added to the generated code)'); + FS.initialized = true; // Allow Module.stdin etc. to provide defaults, if none explicitly passed to us here - Module['stdin'] = input || Module['stdin']; - Module['stdout'] = output || Module['stdout']; - Module['stderr'] = error || Module['stderr']; + input ??= Module['stdin']; + output ??= Module['stdout']; + error ??= Module['stderr']; - FS.createStandardStreams(); + FS.createStandardStreams(input, output, error); }, quit() { - FS.init.initialized = false; + FS.initialized = false; // force-flush all streams, so we get musl std streams printed out _fflush(0); // close all of our streams - for (var i = 0; i < FS.streams.length; i++) { - var stream = FS.streams[i]; - if (!stream) { - continue; + for (var stream of FS.streams) { + if (stream) { + FS.close(stream); } - FS.close(stream); } }, findObject(path, dontResolveLastLink) { @@ -4022,7 +3778,7 @@ function dbg(text) { try { FS.mkdir(current); } catch (e) { - // ignore EEXIST + if (e.errno != 20) throw e; } parent = current; } @@ -4042,11 +3798,7 @@ function dbg(text) { var mode = FS_getMode(canRead, canWrite); var node = FS.create(path, mode); if (data) { - if (typeof data == 'string') { - var arr = new Array(data.length); - for (var i = 0, len = data.length; i < len; ++i) arr[i] = data.charCodeAt(i); - data = arr; - } + data = FS_fileDataToTypedArray(data); // make sure we can write to the file FS.chmod(node, mode | 146); var stream = FS.open(node, 577); @@ -4058,7 +3810,7 @@ function dbg(text) { createDevice(parent, name, input, output) { var path = PATH.join2(typeof parent == 'string' ? parent : FS.getPath(parent), name); var mode = FS_getMode(!!input, !!output); - if (!FS.createDevice.major) FS.createDevice.major = 64; + FS.createDevice.major ??= 64; var dev = FS.makedev(FS.createDevice.major++, 0); // Create a fake device that a set of stream ops to emulate // the old behavior. @@ -4068,7 +3820,7 @@ function dbg(text) { }, close(stream) { // flush any pending line data - if (output && output.buffer && output.buffer.length) { + if (output?.buffer?.length) { output(10); } }, @@ -4089,7 +3841,7 @@ function dbg(text) { buffer[offset+i] = result; } if (bytesRead) { - stream.node.timestamp = Date.now(); + stream.node.atime = Date.now(); } return bytesRead; }, @@ -4102,7 +3854,7 @@ function dbg(text) { } } if (length) { - stream.node.timestamp = Date.now(); + stream.node.mtime = stream.node.ctime = Date.now(); } return i; } @@ -4111,124 +3863,112 @@ function dbg(text) { }, forceLoadFile(obj) { if (obj.isDevice || obj.isFolder || obj.link || obj.contents) return true; - if (typeof XMLHttpRequest != 'undefined') { - throw new Error("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread."); - } else if (read_) { - // Command-line. + if (globalThis.XMLHttpRequest) { + abort("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread."); + } else { // Command-line. try { - // WARNING: Can't read binary files in V8's d8 or tracemonkey's js, as - // read() will try to parse UTF8. - obj.contents = intArrayFromString(read_(obj.url), true); - obj.usedBytes = obj.contents.length; + obj.contents = readBinary(obj.url); } catch (e) { throw new FS.ErrnoError(29); } - } else { - throw new Error('Cannot load without read() or XMLHttpRequest.'); } }, createLazyFile(parent, name, url, canRead, canWrite) { - // Lazy chunked Uint8Array (implements get and length from Uint8Array). Actual getting is abstracted away for eventual reuse. - /** @constructor */ - function LazyUint8Array() { - this.lengthKnown = false; - this.chunks = []; // Loaded chunks. Index is the chunk number - } - LazyUint8Array.prototype.get = /** @this{Object} */ function LazyUint8Array_get(idx) { - if (idx > this.length-1 || idx < 0) { - return undefined; + // Lazy chunked Uint8Array (implements get and length from Uint8Array). + // Actual getting is abstracted away for eventual reuse. + class LazyUint8Array { + lengthKnown = false; + chunks = []; // Loaded chunks. Index is the chunk number + get(idx) { + if (idx > this.length-1 || idx < 0) { + return undefined; + } + var chunkOffset = idx % this.chunkSize; + var chunkNum = (idx / this.chunkSize)|0; + return this.getter(chunkNum)[chunkOffset]; } - var chunkOffset = idx % this.chunkSize; - var chunkNum = (idx / this.chunkSize)|0; - return this.getter(chunkNum)[chunkOffset]; - }; - LazyUint8Array.prototype.setDataGetter = function LazyUint8Array_setDataGetter(getter) { - this.getter = getter; - }; - LazyUint8Array.prototype.cacheLength = function LazyUint8Array_cacheLength() { - // Find length - var xhr = new XMLHttpRequest(); - xhr.open('HEAD', url, false); - xhr.send(null); - if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) throw new Error("Couldn't load " + url + ". Status: " + xhr.status); - var datalength = Number(xhr.getResponseHeader("Content-length")); - var header; - var hasByteServing = (header = xhr.getResponseHeader("Accept-Ranges")) && header === "bytes"; - var usesGzip = (header = xhr.getResponseHeader("Content-Encoding")) && header === "gzip"; - - var chunkSize = 1024*1024; // Chunk size in bytes - - if (!hasByteServing) chunkSize = datalength; - - // Function to get a range from the remote URL. - var doXHR = (from, to) => { - if (from > to) throw new Error("invalid range (" + from + ", " + to + ") or no bytes requested!"); - if (to > datalength-1) throw new Error("only " + datalength + " bytes available! programmer error!"); - - // TODO: Use mozResponseArrayBuffer, responseStream, etc. if available. + setDataGetter(getter) { + this.getter = getter; + } + cacheLength() { + // Find length var xhr = new XMLHttpRequest(); - xhr.open('GET', url, false); - if (datalength !== chunkSize) xhr.setRequestHeader("Range", "bytes=" + from + "-" + to); + xhr.open('HEAD', url, false); + xhr.send(null); + if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) abort("Couldn't load " + url + ". Status: " + xhr.status); + var datalength = Number(xhr.getResponseHeader("Content-length")); + var header; + var hasByteServing = (header = xhr.getResponseHeader("Accept-Ranges")) && header === "bytes"; + var usesGzip = (header = xhr.getResponseHeader("Content-Encoding")) && header === "gzip"; + + var chunkSize = 1024*1024; // Chunk size in bytes + + if (!hasByteServing) chunkSize = datalength; + + // Function to get a range from the remote URL. + var doXHR = (from, to) => { + if (from > to) abort("invalid range (" + from + ", " + to + ") or no bytes requested!"); + if (to > datalength-1) abort("only " + datalength + " bytes available! programmer error!"); + + // TODO: Use mozResponseArrayBuffer, responseStream, etc. if available. + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, false); + if (datalength !== chunkSize) xhr.setRequestHeader("Range", "bytes=" + from + "-" + to); + + // Some hints to the browser that we want binary data. + xhr.responseType = 'arraybuffer'; + if (xhr.overrideMimeType) { + xhr.overrideMimeType('text/plain; charset=x-user-defined'); + } + + xhr.send(null); + if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) abort("Couldn't load " + url + ". Status: " + xhr.status); + if (xhr.response !== undefined) { + return new Uint8Array(/** @type{Array} */(xhr.response || [])); + } + return intArrayFromString(xhr.responseText || '', true); + }; + var lazyArray = this; + lazyArray.setDataGetter((chunkNum) => { + var start = chunkNum * chunkSize; + var end = (chunkNum+1) * chunkSize - 1; // including this byte + end = Math.min(end, datalength-1); // if datalength-1 is selected, this is the last block + if (typeof lazyArray.chunks[chunkNum] == 'undefined') { + lazyArray.chunks[chunkNum] = doXHR(start, end); + } + if (typeof lazyArray.chunks[chunkNum] == 'undefined') abort('doXHR failed!'); + return lazyArray.chunks[chunkNum]; + }); - // Some hints to the browser that we want binary data. - xhr.responseType = 'arraybuffer'; - if (xhr.overrideMimeType) { - xhr.overrideMimeType('text/plain; charset=x-user-defined'); + if (usesGzip || !datalength) { + // if the server uses gzip or doesn't supply the length, we have to download the whole file to get the (uncompressed) length + chunkSize = datalength = 1; // this will force getter(0)/doXHR do download the whole file + datalength = this.getter(0).length; + chunkSize = datalength; + out("LazyFiles on gzip forces download of the whole file when length is accessed"); } - xhr.send(null); - if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) throw new Error("Couldn't load " + url + ". Status: " + xhr.status); - if (xhr.response !== undefined) { - return new Uint8Array(/** @type{Array} */(xhr.response || [])); + this._length = datalength; + this._chunkSize = chunkSize; + this.lengthKnown = true; + } + get length() { + if (!this.lengthKnown) { + this.cacheLength(); } - return intArrayFromString(xhr.responseText || '', true); - }; - var lazyArray = this; - lazyArray.setDataGetter((chunkNum) => { - var start = chunkNum * chunkSize; - var end = (chunkNum+1) * chunkSize - 1; // including this byte - end = Math.min(end, datalength-1); // if datalength-1 is selected, this is the last block - if (typeof lazyArray.chunks[chunkNum] == 'undefined') { - lazyArray.chunks[chunkNum] = doXHR(start, end); + return this._length; + } + get chunkSize() { + if (!this.lengthKnown) { + this.cacheLength(); } - if (typeof lazyArray.chunks[chunkNum] == 'undefined') throw new Error('doXHR failed!'); - return lazyArray.chunks[chunkNum]; - }); - - if (usesGzip || !datalength) { - // if the server uses gzip or doesn't supply the length, we have to download the whole file to get the (uncompressed) length - chunkSize = datalength = 1; // this will force getter(0)/doXHR do download the whole file - datalength = this.getter(0).length; - chunkSize = datalength; - out("LazyFiles on gzip forces download of the whole file when length is accessed"); + return this._chunkSize; } + } - this._length = datalength; - this._chunkSize = chunkSize; - this.lengthKnown = true; - }; - if (typeof XMLHttpRequest != 'undefined') { - if (!ENVIRONMENT_IS_WORKER) throw 'Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc'; + if (globalThis.XMLHttpRequest) { + if (!ENVIRONMENT_IS_WORKER) abort('Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc'); var lazyArray = new LazyUint8Array(); - Object.defineProperties(lazyArray, { - length: { - get: /** @this{Object} */ function() { - if (!this.lengthKnown) { - this.cacheLength(); - } - return this._length; - } - }, - chunkSize: { - get: /** @this{Object} */ function() { - if (!this.lengthKnown) { - this.cacheLength(); - } - return this._chunkSize; - } - } - }); - var properties = { isDevice: false, contents: lazyArray }; } else { var properties = { isDevice: false, url: url }; @@ -4247,19 +3987,17 @@ function dbg(text) { // Add a function that defers querying the file size until it is asked the first time. Object.defineProperties(node, { usedBytes: { - get: /** @this {FSNode} */ function() { return this.contents.length; } + get: function() { return this.contents.length; } } }); // override each stream op with one that tries to force load the lazy file first var stream_ops = {}; - var keys = Object.keys(node.stream_ops); - keys.forEach((key) => { - var fn = node.stream_ops[key]; - stream_ops[key] = function forceLoadLazyFile() { + for (const [key, fn] of Object.entries(node.stream_ops)) { + stream_ops[key] = (...args) => { FS.forceLoadFile(node); - return fn.apply(null, arguments); + return fn(...args); }; - }); + } function writeChunks(stream, buffer, offset, length, position) { var contents = stream.node.contents; if (position >= contents.length) @@ -4295,48 +4033,10 @@ function dbg(text) { node.stream_ops = stream_ops; return node; }, - absolutePath() { - abort('FS.absolutePath has been removed; use PATH_FS.resolve instead'); - }, - createFolder() { - abort('FS.createFolder has been removed; use FS.mkdir instead'); - }, - createLink() { - abort('FS.createLink has been removed; use FS.symlink instead'); - }, - joinPath() { - abort('FS.joinPath has been removed; use PATH.join instead'); - }, - mmapAlloc() { - abort('FS.mmapAlloc has been replaced by the top level function mmapAlloc'); - }, - standardizePath() { - abort('FS.standardizePath has been removed; use PATH.normalize instead'); - }, }; - - /** - * Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the - * emscripten HEAP, returns a copy of that string as a Javascript String object. - * - * @param {number} ptr - * @param {number=} maxBytesToRead - An optional length that specifies the - * maximum number of bytes to read. You can omit this parameter to scan the - * string until the first 0 byte. If maxBytesToRead is passed, and the string - * at [ptr, ptr+maxBytesToReadr[ contains a null byte in the middle, then the - * string will cut short at that byte index (i.e. maxBytesToRead will not - * produce a string of exact length [ptr, ptr+maxBytesToRead[) N.B. mixing - * frequent uses of UTF8ToString() with and without maxBytesToRead may throw - * JS JIT optimizations off, so it is worth to consider consistently using one - * @return {string} - */ - var UTF8ToString = (ptr, maxBytesToRead) => { - assert(typeof ptr == 'number', `UTF8ToString expects a number (got ${typeof ptr})`); - return ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead) : ''; - }; var SYSCALLS = { - DEFAULT_POLLMASK:5, + currentUmask:18, calculateAt(dirfd, path, allowEmpty) { if (PATH.isAbs(path)) { return path; @@ -4355,39 +4055,42 @@ function dbg(text) { } return dir; } - return PATH.join2(dir, path); + return dir + '/' + path; }, - doStat(func, path, buf) { - try { - var stat = func(path); - } catch (e) { - if (e && e.node && PATH.normalize(path) !== PATH.normalize(FS.getPath(e.node))) { - // an error occurred while trying to look up the path; we should just report ENOTDIR - return -54; - } - throw e; - } - HEAP32[((buf)>>2)] = stat.dev; - HEAP32[(((buf)+(4))>>2)] = stat.mode; + writeStat(buf, stat) { + HEAPU32[((buf)>>2)] = stat.dev; + HEAPU32[(((buf)+(4))>>2)] = stat.mode; HEAPU32[(((buf)+(8))>>2)] = stat.nlink; - HEAP32[(((buf)+(12))>>2)] = stat.uid; - HEAP32[(((buf)+(16))>>2)] = stat.gid; - HEAP32[(((buf)+(20))>>2)] = stat.rdev; - (tempI64 = [stat.size>>>0,(tempDouble = stat.size,(+(Math.abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? (+(Math.floor((tempDouble)/4294967296.0)))>>>0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)], HEAP32[(((buf)+(24))>>2)] = tempI64[0],HEAP32[(((buf)+(28))>>2)] = tempI64[1]); + HEAPU32[(((buf)+(12))>>2)] = stat.uid; + HEAPU32[(((buf)+(16))>>2)] = stat.gid; + HEAPU32[(((buf)+(20))>>2)] = stat.rdev; + HEAP64[(((buf)+(24))>>3)] = BigInt(stat.size); HEAP32[(((buf)+(32))>>2)] = 4096; HEAP32[(((buf)+(36))>>2)] = stat.blocks; var atime = stat.atime.getTime(); var mtime = stat.mtime.getTime(); var ctime = stat.ctime.getTime(); - (tempI64 = [Math.floor(atime / 1000)>>>0,(tempDouble = Math.floor(atime / 1000),(+(Math.abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? (+(Math.floor((tempDouble)/4294967296.0)))>>>0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)], HEAP32[(((buf)+(40))>>2)] = tempI64[0],HEAP32[(((buf)+(44))>>2)] = tempI64[1]); - HEAPU32[(((buf)+(48))>>2)] = (atime % 1000) * 1000; - (tempI64 = [Math.floor(mtime / 1000)>>>0,(tempDouble = Math.floor(mtime / 1000),(+(Math.abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? (+(Math.floor((tempDouble)/4294967296.0)))>>>0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)], HEAP32[(((buf)+(56))>>2)] = tempI64[0],HEAP32[(((buf)+(60))>>2)] = tempI64[1]); - HEAPU32[(((buf)+(64))>>2)] = (mtime % 1000) * 1000; - (tempI64 = [Math.floor(ctime / 1000)>>>0,(tempDouble = Math.floor(ctime / 1000),(+(Math.abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? (+(Math.floor((tempDouble)/4294967296.0)))>>>0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)], HEAP32[(((buf)+(72))>>2)] = tempI64[0],HEAP32[(((buf)+(76))>>2)] = tempI64[1]); - HEAPU32[(((buf)+(80))>>2)] = (ctime % 1000) * 1000; - (tempI64 = [stat.ino>>>0,(tempDouble = stat.ino,(+(Math.abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? (+(Math.floor((tempDouble)/4294967296.0)))>>>0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)], HEAP32[(((buf)+(88))>>2)] = tempI64[0],HEAP32[(((buf)+(92))>>2)] = tempI64[1]); + HEAP64[(((buf)+(40))>>3)] = BigInt(Math.floor(atime / 1000)); + HEAPU32[(((buf)+(48))>>2)] = (atime % 1000) * 1000 * 1000; + HEAP64[(((buf)+(56))>>3)] = BigInt(Math.floor(mtime / 1000)); + HEAPU32[(((buf)+(64))>>2)] = (mtime % 1000) * 1000 * 1000; + HEAP64[(((buf)+(72))>>3)] = BigInt(Math.floor(ctime / 1000)); + HEAPU32[(((buf)+(80))>>2)] = (ctime % 1000) * 1000 * 1000; + HEAP64[(((buf)+(88))>>3)] = BigInt(stat.ino); return 0; }, + writeStatFs(buf, stats) { + HEAPU32[(((buf)+(4))>>2)] = stats.bsize; + HEAPU32[(((buf)+(60))>>2)] = stats.bsize; + HEAP64[(((buf)+(8))>>3)] = BigInt(stats.blocks); + HEAP64[(((buf)+(16))>>3)] = BigInt(stats.bfree); + HEAP64[(((buf)+(24))>>3)] = BigInt(stats.bavail); + HEAP64[(((buf)+(32))>>3)] = BigInt(stats.files); + HEAP64[(((buf)+(40))>>3)] = BigInt(stats.ffree); + HEAPU32[(((buf)+(48))>>2)] = stats.fsid; + HEAPU32[(((buf)+(64))>>2)] = stats.flags; // ST_NOSUID + HEAPU32[(((buf)+(56))>>2)] = stats.namelen; + }, doMsync(addr, stream, len, flags, offset) { if (!FS.isFile(stream.node.mode)) { throw new FS.ErrnoError(43); @@ -4399,29 +4102,21 @@ function dbg(text) { var buffer = HEAPU8.slice(addr, addr + len); FS.msync(stream, buffer, offset, len, flags); }, - varargs:undefined, - get() { - assert(SYSCALLS.varargs != undefined); - // the `+` prepended here is necessary to convince the JSCompiler that varargs is indeed a number. - var ret = HEAP32[((+SYSCALLS.varargs)>>2)]; - SYSCALLS.varargs += 4; - return ret; + getStreamFromFD(fd) { + var stream = FS.getStreamChecked(fd); + return stream; }, - getp() { return SYSCALLS.get() }, + varargs:undefined, getStr(ptr) { var ret = UTF8ToString(ptr); return ret; }, - getStreamFromFD(fd) { - var stream = FS.getStreamChecked(fd); - return stream; - }, }; function ___syscall_faccessat(dirfd, path, amode, flags) { try { path = SYSCALLS.getStr(path); - assert(flags === 0); + assert(!flags || flags == 512); path = SYSCALLS.calculateAt(dirfd, path); if (amode & ~7) { // need a valid mode @@ -4445,11 +4140,17 @@ function dbg(text) { return -e.errno; } } + - var setErrNo = (value) => { - HEAP32[((___errno_location())>>2)] = value; - return value; + var syscallGetVarargI = () => { + assert(SYSCALLS.varargs != undefined); + // the `+` prepended here is necessary to convince the JSCompiler that varargs is indeed a number. + var ret = HEAP32[((+SYSCALLS.varargs)>>2)]; + SYSCALLS.varargs += 4; + return ret; }; + var syscallGetVarargP = syscallGetVarargI; + function ___syscall_fcntl64(fd, cmd, varargs) { SYSCALLS.varargs = varargs; @@ -4458,7 +4159,7 @@ function dbg(text) { var stream = SYSCALLS.getStreamFromFD(fd); switch (cmd) { case 0: { - var arg = SYSCALLS.get(); + var arg = syscallGetVarargI(); if (arg < 0) { return -28; } @@ -4466,7 +4167,7 @@ function dbg(text) { arg++; } var newStream; - newStream = FS.createStream(stream, arg); + newStream = FS.dupStream(stream, arg); return newStream.fd; } case 1: @@ -4475,43 +4176,39 @@ function dbg(text) { case 3: return stream.flags; case 4: { - var arg = SYSCALLS.get(); - stream.flags |= arg; + var arg = syscallGetVarargI(); + var mask = 289792; + stream.flags = (stream.flags & ~mask) | (arg & mask); return 0; } - case 5: { - var arg = SYSCALLS.getp(); + case 12: { + var arg = syscallGetVarargP(); var offset = 0; // We're always unlocked. HEAP16[(((arg)+(offset))>>1)] = 2; return 0; } - case 6: - case 7: - return 0; // Pretend that the locking is successful. - case 16: - case 8: - return -28; // These are for sockets. We don't have them fully implemented yet. - case 9: - // musl trusts getown return values, due to a bug where they must be, as they overlap with errors. just return -1 here, so fcntl() returns that, and we set errno ourselves. - setErrNo(28); - return -1; - default: { - return -28; - } + case 13: + case 14: + // Pretend that the locking is successful. These are process-level locks, + // and Emscripten programs are a single process. If we supported linking a + // filesystem between programs, we'd need to do more here. + // See https://github.com/emscripten-core/emscripten/issues/23697 + return 0; } + return -28; } catch (e) { if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; return -e.errno; } } + var stringToUTF8 = (str, outPtr, maxBytesToWrite) => { - assert(typeof maxBytesToWrite == 'number', 'stringToUTF8(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!'); + assert(typeof maxBytesToWrite == 'number', 'stringToUTF8 requires a third parameter that specifies the length of the output buffer'); return stringToUTF8Array(str, HEAPU8, outPtr, maxBytesToWrite); }; - function ___syscall_getcwd(buf, size) { try { @@ -4526,7 +4223,9 @@ function dbg(text) { return -e.errno; } } + + function ___syscall_ioctl(fd, op, varargs) { SYSCALLS.varargs = varargs; try { @@ -4541,13 +4240,13 @@ function dbg(text) { if (!stream.tty) return -59; if (stream.tty.ops.ioctl_tcgets) { var termios = stream.tty.ops.ioctl_tcgets(stream); - var argp = SYSCALLS.getp(); + var argp = syscallGetVarargP(); HEAP32[((argp)>>2)] = termios.c_iflag || 0; HEAP32[(((argp)+(4))>>2)] = termios.c_oflag || 0; HEAP32[(((argp)+(8))>>2)] = termios.c_cflag || 0; HEAP32[(((argp)+(12))>>2)] = termios.c_lflag || 0; for (var i = 0; i < 32; i++) { - HEAP8[(((argp + i)+(17))>>0)] = termios.c_cc[i] || 0; + HEAP8[(argp + i)+(17)] = termios.c_cc[i] || 0; } return 0; } @@ -4564,14 +4263,14 @@ function dbg(text) { case 21508: { if (!stream.tty) return -59; if (stream.tty.ops.ioctl_tcsets) { - var argp = SYSCALLS.getp(); + var argp = syscallGetVarargP(); var c_iflag = HEAP32[((argp)>>2)]; var c_oflag = HEAP32[(((argp)+(4))>>2)]; var c_cflag = HEAP32[(((argp)+(8))>>2)]; var c_lflag = HEAP32[(((argp)+(12))>>2)]; var c_cc = [] for (var i = 0; i < 32; i++) { - c_cc.push(HEAP8[(((argp + i)+(17))>>0)]); + c_cc.push(HEAP8[(argp + i)+(17)]); } return stream.tty.ops.ioctl_tcsets(stream.tty, op, { c_iflag, c_oflag, c_cflag, c_lflag, c_cc }); } @@ -4579,7 +4278,7 @@ function dbg(text) { } case 21519: { if (!stream.tty) return -59; - var argp = SYSCALLS.getp(); + var argp = syscallGetVarargP(); HEAP32[((argp)>>2)] = 0; return 0; } @@ -4587,8 +4286,9 @@ function dbg(text) { if (!stream.tty) return -59; return -28; // not supported } + case 21537: case 21531: { - var argp = SYSCALLS.getp(); + var argp = syscallGetVarargP(); return FS.ioctl(stream, op, argp); } case 21523: { @@ -4597,7 +4297,7 @@ function dbg(text) { if (!stream.tty) return -59; if (stream.tty.ops.ioctl_tiocgwinsz) { var winsize = stream.tty.ops.ioctl_tiocgwinsz(stream.tty); - var argp = SYSCALLS.getp(); + var argp = syscallGetVarargP(); HEAP16[((argp)>>1)] = winsize[0]; HEAP16[(((argp)+(2))>>1)] = winsize[1]; } @@ -4621,20 +4321,26 @@ function dbg(text) { return -e.errno; } } + + function ___syscall_openat(dirfd, path, flags, varargs) { SYSCALLS.varargs = varargs; try { path = SYSCALLS.getStr(path); path = SYSCALLS.calculateAt(dirfd, path); - var mode = varargs ? SYSCALLS.get() : 0; + var mode = varargs ? syscallGetVarargI() : 0; + if (flags & 64) { + mode &= ~SYSCALLS.currentUmask; + } return FS.open(path, flags, mode).fd; } catch (e) { if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; return -e.errno; } } + @@ -4658,6 +4364,10 @@ function dbg(text) { return -e.errno; } } + + + var __abort_js = () => + abort('native code called abort()'); var isLeapYear = (year) => year%4 === 0 && (year%100 !== 0 || year%400 === 0); @@ -4672,15 +4382,14 @@ function dbg(text) { return yday; }; - var convertI32PairToI53Checked = (lo, hi) => { - assert(lo == (lo >>> 0) || lo == (lo|0)); // lo should either be a i32 or a u32 - assert(hi === (hi|0)); // hi should be a i32 - return ((hi + 0x200000) >>> 0 < 0x400001 - !!lo) ? (lo >>> 0) + hi * 4294967296 : NaN; - }; - function __localtime_js(time_low, time_high,tmPtr) { - var time = convertI32PairToI53Checked(time_low, time_high);; + var INT53_MAX = 9007199254740992; + + var INT53_MIN = -9007199254740992; + var bigintToI53Checked = (num) => (num < INT53_MIN || num > INT53_MAX) ? NaN : Number(num); + function __localtime_js(time, tmPtr) { + time = bigintToI53Checked(time); + - var date = new Date(time*1000); HEAP32[((tmPtr)>>2)] = date.getSeconds(); HEAP32[(((tmPtr)+(4))>>2)] = date.getMinutes(); @@ -4704,14 +4413,7 @@ function dbg(text) { } - - var stringToNewUTF8 = (str) => { - var size = lengthBytesUTF8(str) + 1; - var ret = _malloc(size); - if (ret) stringToUTF8(str, ret, size); - return ret; - }; - var __tzset_js = (timezone, daylight, tzname) => { + var __tzset_js = (timezone, daylight, std_name, dst_name) => { // TODO: Use (malleable) environment variables instead of system settings. var currentYear = new Date().getFullYear(); var winter = new Date(currentYear, 0, 1); @@ -4719,9 +4421,12 @@ function dbg(text) { var winterOffset = winter.getTimezoneOffset(); var summerOffset = summer.getTimezoneOffset(); - // Local standard timezone offset. Local standard time is not adjusted for daylight savings. - // This code uses the fact that getTimezoneOffset returns a greater value during Standard Time versus Daylight Saving Time (DST). - // Thus it determines the expected output during Standard Time, and it compares whether the output of the given date the same (Standard) or less (DST). + // Local standard timezone offset. Local standard time is not adjusted for + // daylight savings. This code uses the fact that getTimezoneOffset returns + // a greater value during Standard Time versus Daylight Saving Time (DST). + // Thus it determines the expected output during Standard Time, and it + // compares whether the output of the given date the same (Standard) or less + // (DST). var stdTimezoneOffset = Math.max(winterOffset, summerOffset); // timezone is specified as seconds west of UTC ("The external variable @@ -4733,35 +4438,36 @@ function dbg(text) { HEAP32[((daylight)>>2)] = Number(winterOffset != summerOffset); - function extractZone(date) { - var match = date.toTimeString().match(/\(([A-Za-z ]+)\)$/); - return match ? match[1] : "GMT"; - }; - var winterName = extractZone(winter); - var summerName = extractZone(summer); - var winterNamePtr = stringToNewUTF8(winterName); - var summerNamePtr = stringToNewUTF8(summerName); + var extractZone = (timezoneOffset) => { + // Why inverse sign? + // Read here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset + var sign = timezoneOffset >= 0 ? "-" : "+"; + + var absOffset = Math.abs(timezoneOffset) + var hours = String(Math.floor(absOffset / 60)).padStart(2, "0"); + var minutes = String(absOffset % 60).padStart(2, "0"); + + return `UTC${sign}${hours}${minutes}`; + } + + var winterName = extractZone(winterOffset); + var summerName = extractZone(summerOffset); + assert(winterName); + assert(summerName); + assert(lengthBytesUTF8(winterName) <= 16, `timezone name truncated to fit in TZNAME_MAX (${winterName})`); + assert(lengthBytesUTF8(summerName) <= 16, `timezone name truncated to fit in TZNAME_MAX (${summerName})`); if (summerOffset < winterOffset) { // Northern hemisphere - HEAPU32[((tzname)>>2)] = winterNamePtr; - HEAPU32[(((tzname)+(4))>>2)] = summerNamePtr; + stringToUTF8(winterName, std_name, 17); + stringToUTF8(summerName, dst_name, 17); } else { - HEAPU32[((tzname)>>2)] = summerNamePtr; - HEAPU32[(((tzname)+(4))>>2)] = winterNamePtr; + stringToUTF8(winterName, dst_name, 17); + stringToUTF8(summerName, std_name, 17); } }; - var _abort = () => { - abort('native code called abort()'); - }; - var _emscripten_date_now = () => Date.now(); - var _emscripten_memcpy_js = (dest, src, num) => HEAPU8.copyWithin(dest, src, src + num); - - var getHeapMax = () => - HEAPU8.length; - var abortOnCannotGrowMemory = (requestedSize) => { abort(`Cannot enlarge memory arrays to size ${requestedSize} bytes (OOM). Either (1) compile with -sINITIAL_MEMORY=X with X higher than the current value ${HEAP8.length}, (2) compile with -sALLOW_MEMORY_GROWTH which allows increasing the size at runtime, or (3) if you want malloc to return NULL (0) instead of this abort, compile with -sABORTING_MALLOC=0`); }; @@ -4775,14 +4481,11 @@ function dbg(text) { var ENV = { }; - var getExecutableName = () => { - return thisProgram || './this.program'; - }; + var getExecutableName = () => thisProgram || './this.program'; var getEnvStrings = () => { if (!getEnvStrings.strings) { // Default values. - // Browser language detection #8751 - var lang = ((typeof navigator == 'object' && navigator.languages && navigator.languages[0]) || 'C').replace('-', '_') + '.UTF-8'; + var lang = (globalThis.navigator?.language ?? 'C').replace('-', '_') + '.UTF-8'; var env = { 'USER': 'web_user', 'LOGNAME': 'web_user', @@ -4809,23 +4512,15 @@ function dbg(text) { return getEnvStrings.strings; }; - var stringToAscii = (str, buffer) => { - for (var i = 0; i < str.length; ++i) { - assert(str.charCodeAt(i) === (str.charCodeAt(i) & 0xff)); - HEAP8[((buffer++)>>0)] = str.charCodeAt(i); - } - // Null-terminate the string - HEAP8[((buffer)>>0)] = 0; - }; - var _environ_get = (__environ, environ_buf) => { var bufSize = 0; - getEnvStrings().forEach((string, i) => { + var envp = 0; + for (var string of getEnvStrings()) { var ptr = environ_buf + bufSize; - HEAPU32[(((__environ)+(i*4))>>2)] = ptr; - stringToAscii(string, ptr); - bufSize += string.length + 1; - }); + HEAPU32[(((__environ)+(envp))>>2)] = ptr; + bufSize += stringToUTF8(string, ptr, Infinity) + 1; + envp += 4; + } return 0; }; @@ -4834,7 +4529,9 @@ function dbg(text) { var strings = getEnvStrings(); HEAPU32[((penviron_count)>>2)] = strings.length; var bufSize = 0; - strings.forEach((string) => bufSize += string.length + 1); + for (var string of strings) { + bufSize += lengthBytesUTF8(string) + 1; + } HEAPU32[((penviron_buf_size)>>2)] = bufSize; return 0; }; @@ -4842,17 +4539,16 @@ function dbg(text) { var runtimeKeepaliveCounter = 0; var keepRuntimeAlive = () => noExitRuntime || runtimeKeepaliveCounter > 0; - var _proc_exit = (code) => { EXITSTATUS = code; if (!keepRuntimeAlive()) { - if (Module['onExit']) Module['onExit'](code); + Module['onExit']?.(code); ABORT = true; } quit_(code, new ExitStatus(code)); }; - /** @suppress {duplicate } */ + /** @param {boolean|number=} implicit */ var exitJS = (status, implicit) => { EXITSTATUS = status; @@ -4880,6 +4576,34 @@ function dbg(text) { return e.errno; } } + + + function _fd_fdstat_get(fd, pbuf) { + try { + + var rightsBase = 0; + var rightsInheriting = 0; + var flags = 0; + { + var stream = SYSCALLS.getStreamFromFD(fd); + // All character devices are terminals (other things a Linux system would + // assume is a character device, like the mouse, we have special APIs for). + var type = stream.tty ? 2 : + FS.isDir(stream.mode) ? 3 : + FS.isLink(stream.mode) ? 7 : + 4; + } + HEAP8[pbuf] = type; + HEAP16[(((pbuf)+(2))>>1)] = flags; + HEAP64[(((pbuf)+(8))>>3)] = BigInt(rightsBase); + HEAP64[(((pbuf)+(16))>>3)] = BigInt(rightsInheriting); + return 0; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return e.errno; + } + } + /** @param {number=} offset */ var doReadv = (stream, iov, iovcnt, offset) => { @@ -4892,7 +4616,7 @@ function dbg(text) { if (curr < 0) return -1; ret += curr; if (curr < len) break; // nothing more to read - if (typeof offset !== 'undefined') { + if (typeof offset != 'undefined') { offset += curr; } } @@ -4911,18 +4635,19 @@ function dbg(text) { return e.errno; } } + - function _fd_seek(fd,offset_low, offset_high,whence,newOffset) { - var offset = convertI32PairToI53Checked(offset_low, offset_high);; + function _fd_seek(fd, offset, whence, newOffset) { + offset = bigintToI53Checked(offset); + - try { - if (isNaN(offset)) return 61; + if (isNaN(offset)) return 22; var stream = SYSCALLS.getStreamFromFD(fd); FS.llseek(stream, offset, whence); - (tempI64 = [stream.position>>>0,(tempDouble = stream.position,(+(Math.abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? (+(Math.floor((tempDouble)/4294967296.0)))>>>0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)], HEAP32[((newOffset)>>2)] = tempI64[0],HEAP32[(((newOffset)+(4))>>2)] = tempI64[1]); + HEAP64[((newOffset)>>3)] = BigInt(stream.position); if (stream.getdents && offset === 0 && whence === 0) stream.getdents = null; // reset readdir state return 0; } catch (e) { @@ -4942,7 +4667,11 @@ function dbg(text) { var curr = FS.write(stream, HEAP8, ptr, len, offset); if (curr < 0) return -1; ret += curr; - if (typeof offset !== 'undefined') { + if (curr < len) { + // No more space to write. + break; + } + if (typeof offset != 'undefined') { offset += curr; } } @@ -4961,6 +4690,7 @@ function dbg(text) { return e.errno; } } + var handleException = (e) => { @@ -4982,6 +4712,8 @@ function dbg(text) { }; + + var stackAlloc = (sz) => __emscripten_stack_alloc(sz); var stringToUTF8OnStack = (str) => { var size = lengthBytesUTF8(str) + 1; var ret = stackAlloc(size); @@ -4991,154 +4723,121 @@ function dbg(text) { + var FS_createPath = (...args) => FS.createPath(...args); + + + + var FS_unlink = (...args) => FS.unlink(...args); + + var FS_createLazyFile = (...args) => FS.createLazyFile(...args); + + var FS_createDevice = (...args) => FS.createDevice(...args); - var FS_unlink = (path) => FS.unlink(path); - var FSNode = /** @constructor */ function(parent, name, mode, rdev) { - if (!parent) { - parent = this; // root node sets parent to itself - } - this.parent = parent; - this.mount = parent.mount; - this.mounted = null; - this.id = FS.nextInode++; - this.name = name; - this.mode = mode; - this.node_ops = {}; - this.stream_ops = {}; - this.rdev = rdev; - }; - var readMode = 292/*292*/ | 73/*73*/; - var writeMode = 146/*146*/; - Object.defineProperties(FSNode.prototype, { - read: { - get: /** @this{FSNode} */function() { - return (this.mode & readMode) === readMode; - }, - set: /** @this{FSNode} */function(val) { - val ? this.mode |= readMode : this.mode &= ~readMode; - } - }, - write: { - get: /** @this{FSNode} */function() { - return (this.mode & writeMode) === writeMode; - }, - set: /** @this{FSNode} */function(val) { - val ? this.mode |= writeMode : this.mode &= ~writeMode; - } - }, - isFolder: { - get: /** @this{FSNode} */function() { - return FS.isDir(this.mode); - } - }, - isDevice: { - get: /** @this{FSNode} */function() { - return FS.isChrdev(this.mode); - } - } - }); - FS.FSNode = FSNode; FS.createPreloadedFile = FS_createPreloadedFile; - FS.staticInit();Module["FS_createPath"] = FS.createPath;Module["FS_createDataFile"] = FS.createDataFile;Module["FS_createPreloadedFile"] = FS.createPreloadedFile;Module["FS_unlink"] = FS.unlink;Module["FS_createLazyFile"] = FS.createLazyFile;Module["FS_createDevice"] = FS.createDevice;; + FS.preloadFile = FS_preloadFile; + FS.staticInit();; if (ENVIRONMENT_IS_NODE) { NODEFS.staticInit(); }; - if (ENVIRONMENT_IS_NODE) { - var _wrapNodeError = function(func) { - return function() { - try { - return func.apply(this, arguments) - } catch (e) { - if (e.code) { - throw new FS.ErrnoError(ERRNO_CODES[e.code]); - } - throw e; + if (!ENVIRONMENT_IS_NODE) { + throw new Error("NODERAWFS is currently only supported on Node.js environment.") + } + var nodeTTY = require('node:tty'); + function _wrapNodeError(func) { + return (...args) => { + try { + return func(...args) + } catch (e) { + // Hack for Deno which throws BadResource instead of EBADF: + // https://github.com/emscripten-core/emscripten/issues/26239 + if (e.name == 'BadResource') { + e.code = 'EBADF'; } + if (e.code) { + throw new FS.ErrnoError(ERRNO_CODES[e.code]); + } + throw e; } - }; - var VFS = Object.assign({}, FS); - for (var _key in NODERAWFS) { - FS[_key] = _wrapNodeError(NODERAWFS[_key]); } - } else { - throw new Error("NODERAWFS is currently only supported on Node.js environment.") + } + function _wrapNodeStreamFunc(func, vfs_func) { + return _wrapNodeError((stream, ...args) => { + if (stream.stream_ops) { + // this stream was created by some other FS. e.g: PIPEFS. + return vfs_func(stream, ...args); + } + return func(stream, ...args); + }); + } + // Use this to reference our in-memory filesystem + /** @suppress {partialAlias} */ + var VFS = {...FS}; + // Wrap the whole in-memory filesystem API with + // our Node.js based functions + for (const [key, value] of Object.entries(NODERAWFS)) { + FS[key] = _wrapNodeError(value); + } + for (const [key, value] of Object.entries(NODERAWFS_stream_funcs)) { + FS[key] = _wrapNodeStreamFunc(value, FS[key]); }; -function checkIncomingModuleAPI() { - ignoredModuleProp('fetchSettings'); -} -var wasmImports = { - /** @export */ - __syscall_faccessat: ___syscall_faccessat, - /** @export */ - __syscall_fcntl64: ___syscall_fcntl64, - /** @export */ - __syscall_getcwd: ___syscall_getcwd, - /** @export */ - __syscall_ioctl: ___syscall_ioctl, - /** @export */ - __syscall_openat: ___syscall_openat, - /** @export */ - __syscall_readlinkat: ___syscall_readlinkat, - /** @export */ - _localtime_js: __localtime_js, - /** @export */ - _tzset_js: __tzset_js, - /** @export */ - abort: _abort, - /** @export */ - emscripten_date_now: _emscripten_date_now, - /** @export */ - emscripten_memcpy_js: _emscripten_memcpy_js, - /** @export */ - emscripten_resize_heap: _emscripten_resize_heap, - /** @export */ - environ_get: _environ_get, - /** @export */ - environ_sizes_get: _environ_sizes_get, - /** @export */ - exit: _exit, - /** @export */ - fd_close: _fd_close, - /** @export */ - fd_read: _fd_read, - /** @export */ - fd_seek: _fd_seek, - /** @export */ - fd_write: _fd_write -}; -var wasmExports = createWasm(); -var ___wasm_call_ctors = createExportWrapper('__wasm_call_ctors'); -var ___errno_location = createExportWrapper('__errno_location'); -var _main = Module['_main'] = createExportWrapper('__main_argc_argv'); -var _malloc = createExportWrapper('malloc'); -var _free = createExportWrapper('free'); -var _fflush = Module['_fflush'] = createExportWrapper('fflush'); -var setTempRet0 = createExportWrapper('setTempRet0'); -var _emscripten_stack_init = () => (_emscripten_stack_init = wasmExports['emscripten_stack_init'])(); -var _emscripten_stack_get_free = () => (_emscripten_stack_get_free = wasmExports['emscripten_stack_get_free'])(); -var _emscripten_stack_get_base = () => (_emscripten_stack_get_base = wasmExports['emscripten_stack_get_base'])(); -var _emscripten_stack_get_end = () => (_emscripten_stack_get_end = wasmExports['emscripten_stack_get_end'])(); -var stackSave = createExportWrapper('stackSave'); -var stackRestore = createExportWrapper('stackRestore'); -var stackAlloc = createExportWrapper('stackAlloc'); -var _emscripten_stack_get_current = () => (_emscripten_stack_get_current = wasmExports['emscripten_stack_get_current'])(); -var dynCall_jiji = Module['dynCall_jiji'] = createExportWrapper('dynCall_jiji'); +// End JS library code +// include: postlibrary.js +// This file is included after the automatically-generated JS library code +// but before the wasm module is created. -// include: postamble.js -// === Auto-generated postamble setup entry stuff === +{ + + // Begin ATMODULES hooks + if (Module['noExitRuntime']) noExitRuntime = Module['noExitRuntime']; +if (Module['preloadPlugins']) preloadPlugins = Module['preloadPlugins']; +if (Module['print']) out = Module['print']; +if (Module['printErr']) err = Module['printErr']; +if (Module['wasmBinary']) wasmBinary = Module['wasmBinary']; + // End ATMODULES hooks + + checkIncomingModuleAPI(); + + if (Module['arguments']) arguments_ = Module['arguments']; + if (Module['thisProgram']) thisProgram = Module['thisProgram']; + + // Assertions on removed incoming Module JS APIs. + assert(typeof Module['memoryInitializerPrefixURL'] == 'undefined', 'Module.memoryInitializerPrefixURL option was removed, use Module.locateFile instead'); + assert(typeof Module['pthreadMainPrefixURL'] == 'undefined', 'Module.pthreadMainPrefixURL option was removed, use Module.locateFile instead'); + assert(typeof Module['cdInitializerPrefixURL'] == 'undefined', 'Module.cdInitializerPrefixURL option was removed, use Module.locateFile instead'); + assert(typeof Module['filePackagePrefixURL'] == 'undefined', 'Module.filePackagePrefixURL option was removed, use Module.locateFile instead'); + assert(typeof Module['read'] == 'undefined', 'Module.read option was removed'); + assert(typeof Module['readAsync'] == 'undefined', 'Module.readAsync option was removed (modify readAsync in JS)'); + assert(typeof Module['readBinary'] == 'undefined', 'Module.readBinary option was removed (modify readBinary in JS)'); + assert(typeof Module['setWindowTitle'] == 'undefined', 'Module.setWindowTitle option was removed (modify emscripten_set_window_title in JS)'); + assert(typeof Module['TOTAL_MEMORY'] == 'undefined', 'Module.TOTAL_MEMORY has been renamed Module.INITIAL_MEMORY'); + assert(typeof Module['ENVIRONMENT'] == 'undefined', 'Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -sENVIRONMENT=web or -sENVIRONMENT=node)'); + assert(typeof Module['STACK_SIZE'] == 'undefined', 'STACK_SIZE can no longer be set at runtime. Use -sSTACK_SIZE at link time') + // If memory is defined in wasm, the user can't provide it, or set INITIAL_MEMORY + assert(typeof Module['wasmMemory'] == 'undefined', 'Use of `wasmMemory` detected. Use -sIMPORTED_MEMORY to define wasmMemory externally'); + assert(typeof Module['INITIAL_MEMORY'] == 'undefined', 'Detected runtime INITIAL_MEMORY setting. Use -sIMPORTED_MEMORY to define wasmMemory dynamically'); + + if (Module['preInit']) { + if (typeof Module['preInit'] == 'function') Module['preInit'] = [Module['preInit']]; + while (Module['preInit'].length > 0) { + Module['preInit'].shift()(); + } + } + consumedModuleProp('preInit'); +} -Module['addRunDependency'] = addRunDependency; -Module['removeRunDependency'] = removeRunDependency; -Module['FS_createPath'] = FS.createPath; -Module['FS_createLazyFile'] = FS.createLazyFile; -Module['FS_createDevice'] = FS.createDevice; -Module['FS_createPreloadedFile'] = FS.createPreloadedFile; -Module['FS'] = FS; -Module['FS_createDataFile'] = FS.createDataFile; -Module['FS_unlink'] = FS.unlink; -var missingLibrarySymbols = [ +// Begin runtime exports + Module['addRunDependency'] = addRunDependency; + Module['removeRunDependency'] = removeRunDependency; + Module['FS_preloadFile'] = FS_preloadFile; + Module['FS_unlink'] = FS_unlink; + Module['FS_createPath'] = FS_createPath; + Module['FS_createDevice'] = FS_createDevice; + Module['FS'] = FS; + Module['FS_createDataFile'] = FS_createDataFile; + Module['FS_createLazyFile'] = FS_createLazyFile; + var missingLibrarySymbols = [ 'writeI53ToI64', 'writeI53ToI64Clamped', 'writeI53ToI64Signaling', @@ -5147,26 +4846,24 @@ var missingLibrarySymbols = [ 'readI53FromI64', 'readI53FromU64', 'convertI32PairToI53', + 'convertI32PairToI53Checked', 'convertU32PairToI53', + 'getTempRet0', + 'setTempRet0', + 'createNamedFunction', + 'zeroMemory', + 'getHeapMax', 'growMemory', - 'arraySum', - 'addDays', + 'withStackSave', 'inetPton4', 'inetNtop4', 'inetPton6', 'inetNtop6', 'readSockaddr', 'writeSockaddr', - 'getHostByName', - 'getCallstack', - 'emscriptenLog', - 'convertPCtoSourceLocation', 'readEmAsmArgs', 'jstoi_q', - 'jstoi_s', - 'listenOnce', 'autoResumeAudioContext', - 'dynCallLegacy', 'getDynCaller', 'dynCall', 'runtimeKeepalivePush', @@ -5174,43 +4871,38 @@ var missingLibrarySymbols = [ 'callUserCallback', 'maybeExit', 'asmjsMangle', - 'handleAllocatorInit', + 'alignMemory', 'HandleAllocator', - 'getNativeTypeSize', + 'addOnInit', + 'addOnPostCtor', + 'addOnPreMain', + 'addOnExit', 'STACK_SIZE', 'STACK_ALIGN', 'POINTER_SIZE', 'ASSERTIONS', - 'getCFunc', 'ccall', 'cwrap', - 'uleb128Encode', - 'sigToWasmTypes', - 'generateFuncType', 'convertJsFunctionToWasm', 'getEmptyTableSlot', 'updateTableMap', 'getFunctionAddress', 'addFunction', 'removeFunction', - 'reallyNegative', - 'unSign', - 'strLen', - 'reSign', - 'formatString', 'intArrayToString', 'AsciiToString', + 'stringToAscii', 'UTF16ToString', 'stringToUTF16', 'lengthBytesUTF16', 'UTF32ToString', 'stringToUTF32', 'lengthBytesUTF32', + 'stringToNewUTF8', 'writeArrayToMemory', 'registerKeyEventCallback', 'maybeCStringToJsString', 'findEventTarget', - 'findCanvasEventTarget', 'getBoundingClientRect', 'fillMouseEventData', 'registerMouseEventCallback', @@ -5245,42 +4937,49 @@ var missingLibrarySymbols = [ 'registerGamepadEventCallback', 'registerBeforeUnloadEventCallback', 'fillBatteryEventData', - 'battery', 'registerBatteryEventCallback', 'setCanvasElementSize', 'getCanvasElementSize', 'jsStackTrace', - 'stackTrace', + 'getCallstack', + 'convertPCtoSourceLocation', 'checkWasiClock', 'wasiRightsToMuslOFlags', 'wasiOFlagsToMuslOFlags', - 'createDyncallWrapper', 'safeSetTimeout', 'setImmediateWrapped', + 'safeRequestAnimationFrame', 'clearImmediateWrapped', - 'polyfillSetImmediate', + 'registerPostMainLoop', + 'registerPreMainLoop', 'getPromise', 'makePromise', 'idsToPromises', 'makePromiseCallback', 'ExceptionInfo', 'findMatchingCatch', - 'setMainLoop', + 'incrementUncaughtExceptionCount', + 'decrementUncaughtExceptionCount', + 'Browser_asyncPrepareDataCounter', + 'arraySum', + 'addDays', 'getSocketFromFD', 'getSocketAddress', 'FS_mkdirTree', '_setNetworkCallback', 'heapObjectForWebGLType', - 'heapAccessShiftForWebGLHeap', + 'toTypedArrayIndex', 'webgl_enable_ANGLE_instanced_arrays', 'webgl_enable_OES_vertex_array_object', 'webgl_enable_WEBGL_draw_buffers', 'webgl_enable_WEBGL_multi_draw', + 'webgl_enable_EXT_polygon_offset_clamp', + 'webgl_enable_EXT_clip_control', + 'webgl_enable_WEBGL_polygon_mode', 'emscriptenWebGLGet', 'computeUnpackAlignedImageSize', 'colorChannelsInGlTextureFormat', 'emscriptenWebGLGetTexPixelData', - '__glGenObject', 'emscriptenWebGLGetUniform', 'webglGetUniformLocation', 'webglPrepareUniformLocationsBeforeFirstUse', @@ -5290,73 +4989,67 @@ var missingLibrarySymbols = [ 'writeGLArray', 'registerWebGlEventCallback', 'runAndAbortIfError', - 'SDL_unicode', - 'SDL_ttfContext', - 'SDL_audio', 'ALLOC_NORMAL', 'ALLOC_STACK', 'allocate', 'writeStringToMemory', 'writeAsciiToMemory', + 'allocateUTF8', + 'allocateUTF8OnStack', + 'demangle', + 'stackTrace', + 'getNativeTypeSize', ]; missingLibrarySymbols.forEach(missingLibrarySymbol) -var unexportedSymbols = [ + var unexportedSymbols = [ 'run', - 'addOnPreRun', - 'addOnInit', - 'addOnPreMain', - 'addOnExit', - 'addOnPostRun', - 'FS_createFolder', - 'FS_createLink', - 'FS_readFile', 'out', 'err', 'callMain', 'abort', - 'wasmMemory', 'wasmExports', - 'stackAlloc', - 'stackSave', - 'stackRestore', - 'getTempRet0', - 'setTempRet0', 'writeStackCookie', 'checkStackCookie', - 'convertI32PairToI53Checked', + 'INT53_MAX', + 'INT53_MIN', + 'bigintToI53Checked', + 'HEAP8', + 'HEAPU8', + 'HEAP16', + 'HEAPU16', + 'HEAP32', + 'HEAPU32', + 'HEAPF32', + 'HEAPF64', + 'HEAP64', + 'HEAPU64', + 'stackSave', + 'stackRestore', + 'stackAlloc', 'ptrToString', - 'zeroMemory', 'exitJS', - 'getHeapMax', 'abortOnCannotGrowMemory', 'ENV', - 'MONTH_DAYS_REGULAR', - 'MONTH_DAYS_LEAP', - 'MONTH_DAYS_REGULAR_CUMULATIVE', - 'MONTH_DAYS_LEAP_CUMULATIVE', - 'isLeapYear', - 'ydayFromDate', 'ERRNO_CODES', - 'ERRNO_MESSAGES', - 'setErrNo', + 'strError', 'DNS', 'Protocols', 'Sockets', - 'initRandomFill', - 'randomFill', 'timers', 'warnOnce', - 'UNWIND_CACHE', 'readEmAsmArgsArray', 'getExecutableName', 'handleException', 'keepRuntimeAlive', 'asyncLoad', - 'alignMemory', 'mmapAlloc', 'wasmTable', + 'wasmMemory', + 'getUniqueRunDependency', 'noExitRuntime', + 'addOnPreRun', + 'addOnPostRun', 'freeTableIndexes', 'functionsInTableMap', 'setValue', @@ -5370,32 +5063,152 @@ var unexportedSymbols = [ 'stringToUTF8', 'lengthBytesUTF8', 'intArrayFromString', - 'stringToAscii', 'UTF16Decoder', - 'stringToNewUTF8', 'stringToUTF8OnStack', 'JSEvents', 'specialHTMLTargets', + 'findCanvasEventTarget', 'currentFullscreenStrategy', 'restoreOldWindowedStyle', - 'demangle', - 'demangleAll', + 'UNWIND_CACHE', 'ExitStatus', 'getEnvStrings', 'doReadv', 'doWritev', + 'initRandomFill', + 'randomFill', + 'emSetImmediate', + 'emClearImmediate_deps', + 'emClearImmediate', 'promiseMap', 'uncaughtExceptionCount', - 'exceptionLast', 'exceptionCaught', 'Browser', + 'requestFullscreen', + 'requestFullScreen', + 'setCanvasSize', + 'getUserMedia', + 'createContext', + 'getPreloadedImageData__data', 'wget', + 'MONTH_DAYS_REGULAR', + 'MONTH_DAYS_LEAP', + 'MONTH_DAYS_REGULAR_CUMULATIVE', + 'MONTH_DAYS_LEAP_CUMULATIVE', + 'isLeapYear', + 'ydayFromDate', 'SYSCALLS', 'preloadPlugins', + 'FS_createPreloadedFile', 'FS_modeStringToFlags', 'FS_getMode', + 'FS_fileDataToTypedArray', 'FS_stdin_getChar_buffer', 'FS_stdin_getChar', + 'FS_readFile', + 'FS_root', + 'FS_mounts', + 'FS_devices', + 'FS_streams', + 'FS_nextInode', + 'FS_nameTable', + 'FS_currentPath', + 'FS_initialized', + 'FS_ignorePermissions', + 'FS_filesystems', + 'FS_syncFSRequests', + 'FS_lookupPath', + 'FS_getPath', + 'FS_hashName', + 'FS_hashAddNode', + 'FS_hashRemoveNode', + 'FS_lookupNode', + 'FS_createNode', + 'FS_destroyNode', + 'FS_isRoot', + 'FS_isMountpoint', + 'FS_isFile', + 'FS_isDir', + 'FS_isLink', + 'FS_isChrdev', + 'FS_isBlkdev', + 'FS_isFIFO', + 'FS_isSocket', + 'FS_flagsToPermissionString', + 'FS_nodePermissions', + 'FS_mayLookup', + 'FS_mayCreate', + 'FS_mayDelete', + 'FS_mayOpen', + 'FS_checkOpExists', + 'FS_nextfd', + 'FS_getStreamChecked', + 'FS_getStream', + 'FS_createStream', + 'FS_closeStream', + 'FS_dupStream', + 'FS_doSetAttr', + 'FS_chrdev_stream_ops', + 'FS_major', + 'FS_minor', + 'FS_makedev', + 'FS_registerDevice', + 'FS_getDevice', + 'FS_getMounts', + 'FS_syncfs', + 'FS_mount', + 'FS_unmount', + 'FS_lookup', + 'FS_mknod', + 'FS_statfs', + 'FS_statfsStream', + 'FS_statfsNode', + 'FS_create', + 'FS_mkdir', + 'FS_mkdev', + 'FS_symlink', + 'FS_rename', + 'FS_rmdir', + 'FS_readdir', + 'FS_readlink', + 'FS_stat', + 'FS_fstat', + 'FS_lstat', + 'FS_doChmod', + 'FS_chmod', + 'FS_lchmod', + 'FS_fchmod', + 'FS_doChown', + 'FS_chown', + 'FS_lchown', + 'FS_fchown', + 'FS_doTruncate', + 'FS_truncate', + 'FS_ftruncate', + 'FS_utime', + 'FS_open', + 'FS_close', + 'FS_isClosed', + 'FS_llseek', + 'FS_read', + 'FS_write', + 'FS_mmap', + 'FS_msync', + 'FS_ioctl', + 'FS_writeFile', + 'FS_cwd', + 'FS_chdir', + 'FS_createDefaultDirectories', + 'FS_createDefaultDevices', + 'FS_createSpecialDirectories', + 'FS_createStandardStreams', + 'FS_staticInit', + 'FS_init', + 'FS_quit', + 'FS_findObject', + 'FS_analyzePath', + 'FS_createFile', + 'FS_forceLoadFile', 'MEMFS', 'TTY', 'PIPEFS', @@ -5404,7 +5217,6 @@ var unexportedSymbols = [ 'miniTempWebGLFloatBuffers', 'miniTempWebGLIntBuffers', 'GL', - 'emscripten_webgl_power_preferences', 'AL', 'GLUT', 'EGL', @@ -5412,26 +5224,123 @@ var unexportedSymbols = [ 'IDBStore', 'SDL', 'SDL_gfx', - 'allocateUTF8', - 'allocateUTF8OnStack', + 'print', + 'printErr', + 'jstoi_s', 'NODEFS', 'NODERAWFS', + 'NODERAWFS_stream_funcs', ]; unexportedSymbols.forEach(unexportedRuntimeSymbol); + // End runtime exports + // Begin JS library exports + // End JS library exports +// end include: postlibrary.js -var calledRun; +function checkIncomingModuleAPI() { + ignoredModuleProp('fetchSettings'); + ignoredModuleProp('logReadFiles'); + ignoredModuleProp('loadSplitModule'); + ignoredModuleProp('onMalloc'); + ignoredModuleProp('onRealloc'); + ignoredModuleProp('onFree'); + ignoredModuleProp('onSbrkGrow'); +} + +// Imports from the Wasm binary. +var _strerror = makeInvalidEarlyAccess('_strerror'); +var _main = Module['_main'] = makeInvalidEarlyAccess('_main'); +var _fflush = makeInvalidEarlyAccess('_fflush'); +var _emscripten_stack_get_end = makeInvalidEarlyAccess('_emscripten_stack_get_end'); +var _emscripten_stack_get_base = makeInvalidEarlyAccess('_emscripten_stack_get_base'); +var _emscripten_stack_init = makeInvalidEarlyAccess('_emscripten_stack_init'); +var _emscripten_stack_get_free = makeInvalidEarlyAccess('_emscripten_stack_get_free'); +var __emscripten_stack_restore = makeInvalidEarlyAccess('__emscripten_stack_restore'); +var __emscripten_stack_alloc = makeInvalidEarlyAccess('__emscripten_stack_alloc'); +var _emscripten_stack_get_current = makeInvalidEarlyAccess('_emscripten_stack_get_current'); +var memory = makeInvalidEarlyAccess('memory'); +var __indirect_function_table = makeInvalidEarlyAccess('__indirect_function_table'); +var wasmMemory = makeInvalidEarlyAccess('wasmMemory'); + +function assignWasmExports(wasmExports) { + assert(typeof wasmExports['strerror'] != 'undefined', 'missing Wasm export: strerror'); + assert(typeof wasmExports['__main_argc_argv'] != 'undefined', 'missing Wasm export: __main_argc_argv'); + assert(typeof wasmExports['fflush'] != 'undefined', 'missing Wasm export: fflush'); + assert(typeof wasmExports['emscripten_stack_get_end'] != 'undefined', 'missing Wasm export: emscripten_stack_get_end'); + assert(typeof wasmExports['emscripten_stack_get_base'] != 'undefined', 'missing Wasm export: emscripten_stack_get_base'); + assert(typeof wasmExports['emscripten_stack_init'] != 'undefined', 'missing Wasm export: emscripten_stack_init'); + assert(typeof wasmExports['emscripten_stack_get_free'] != 'undefined', 'missing Wasm export: emscripten_stack_get_free'); + assert(typeof wasmExports['_emscripten_stack_restore'] != 'undefined', 'missing Wasm export: _emscripten_stack_restore'); + assert(typeof wasmExports['_emscripten_stack_alloc'] != 'undefined', 'missing Wasm export: _emscripten_stack_alloc'); + assert(typeof wasmExports['emscripten_stack_get_current'] != 'undefined', 'missing Wasm export: emscripten_stack_get_current'); + assert(typeof wasmExports['memory'] != 'undefined', 'missing Wasm export: memory'); + assert(typeof wasmExports['__indirect_function_table'] != 'undefined', 'missing Wasm export: __indirect_function_table'); + _strerror = createExportWrapper('strerror', 1); + _main = Module['_main'] = createExportWrapper('__main_argc_argv', 2); + _fflush = createExportWrapper('fflush', 1); + _emscripten_stack_get_end = wasmExports['emscripten_stack_get_end']; + _emscripten_stack_get_base = wasmExports['emscripten_stack_get_base']; + _emscripten_stack_init = wasmExports['emscripten_stack_init']; + _emscripten_stack_get_free = wasmExports['emscripten_stack_get_free']; + __emscripten_stack_restore = wasmExports['_emscripten_stack_restore']; + __emscripten_stack_alloc = wasmExports['_emscripten_stack_alloc']; + _emscripten_stack_get_current = wasmExports['emscripten_stack_get_current']; + memory = wasmMemory = wasmExports['memory']; + __indirect_function_table = wasmExports['__indirect_function_table']; +} -dependenciesFulfilled = function runCaller() { - // If run has never been called, and we should call run (INVOKE_RUN is true, and Module.noInitialRun is not false) - if (!calledRun) run(); - if (!calledRun) dependenciesFulfilled = runCaller; // try this again later, after new deps are fulfilled +var wasmImports = { + /** @export */ + __syscall_faccessat: ___syscall_faccessat, + /** @export */ + __syscall_fcntl64: ___syscall_fcntl64, + /** @export */ + __syscall_getcwd: ___syscall_getcwd, + /** @export */ + __syscall_ioctl: ___syscall_ioctl, + /** @export */ + __syscall_openat: ___syscall_openat, + /** @export */ + __syscall_readlinkat: ___syscall_readlinkat, + /** @export */ + _abort_js: __abort_js, + /** @export */ + _localtime_js: __localtime_js, + /** @export */ + _tzset_js: __tzset_js, + /** @export */ + emscripten_date_now: _emscripten_date_now, + /** @export */ + emscripten_resize_heap: _emscripten_resize_heap, + /** @export */ + environ_get: _environ_get, + /** @export */ + environ_sizes_get: _environ_sizes_get, + /** @export */ + exit: _exit, + /** @export */ + fd_close: _fd_close, + /** @export */ + fd_fdstat_get: _fd_fdstat_get, + /** @export */ + fd_read: _fd_read, + /** @export */ + fd_seek: _fd_seek, + /** @export */ + fd_write: _fd_write }; + +// include: postamble.js +// === Auto-generated postamble setup entry stuff === + +var calledRun; + function callMain(args = []) { assert(runDependencies == 0, 'cannot call main when async dependencies remain! (listen on Module["onRuntimeInitialized"])'); - assert(__ATPRERUN__.length == 0, 'cannot call main when preRun functions remain to be called'); + assert(typeof onPreRuns === 'undefined' || onPreRuns.length == 0, 'cannot call main when preRun functions remain to be called'); var entryFunction = _main; @@ -5440,10 +5349,10 @@ function callMain(args = []) { var argc = args.length; var argv = stackAlloc((argc + 1) * 4); var argv_ptr = argv; - args.forEach((arg) => { + for (var arg of args) { HEAPU32[((argv_ptr)>>2)] = stringToUTF8OnStack(arg); argv_ptr += 4; - }); + } HEAPU32[((argv_ptr)>>2)] = 0; try { @@ -5453,8 +5362,7 @@ function callMain(args = []) { // if we're not running an evented main loop, it's time to exit exitJS(ret, /* implicit = */ true); return ret; - } - catch (e) { + } catch (e) { return handleException(e); } } @@ -5471,22 +5379,24 @@ function stackCheckInit() { function run(args = arguments_) { if (runDependencies > 0) { + dependenciesFulfilled = run; return; } - stackCheckInit(); + stackCheckInit(); preRun(); // a preRun added a dependency, run will be called later if (runDependencies > 0) { + dependenciesFulfilled = run; return; } function doRun() { // run may have just been called through dependencies being fulfilled just in this very frame, // or while the async setStatus time below was happening - if (calledRun) return; + assert(!calledRun); calledRun = true; Module['calledRun'] = true; @@ -5496,19 +5406,19 @@ function run(args = arguments_) { preMain(); - if (Module['onRuntimeInitialized']) Module['onRuntimeInitialized'](); + Module['onRuntimeInitialized']?.(); + consumedModuleProp('onRuntimeInitialized'); - if (shouldRunNow) callMain(args); + var noInitialRun = Module['noInitialRun'] || false; + if (!noInitialRun) callMain(args); postRun(); } if (Module['setStatus']) { Module['setStatus']('Running...'); - setTimeout(function() { - setTimeout(function() { - Module['setStatus'](''); - }, 1); + setTimeout(() => { + setTimeout(() => Module['setStatus'](''), 1); doRun(); }, 1); } else @@ -5539,16 +5449,16 @@ function checkUnflushedContent() { try { // it doesn't matter if it fails _fflush(0); // also flush in the JS FS layer - ['stdout', 'stderr'].forEach(function(name) { + for (var name of ['stdout', 'stderr']) { var info = FS.analyzePath('/dev/' + name); if (!info) return; var stream = info.object; var rdev = stream.rdev; var tty = TTY.ttys[rdev]; - if (tty && tty.output && tty.output.length) { + if (tty?.output?.length) { has = true; } - }); + } } catch(e) {} out = oldOut; err = oldErr; @@ -5557,19 +5467,13 @@ function checkUnflushedContent() { } } -if (Module['preInit']) { - if (typeof Module['preInit'] == 'function') Module['preInit'] = [Module['preInit']]; - while (Module['preInit'].length > 0) { - Module['preInit'].pop()(); - } -} - -// shouldRunNow refers to calling main(), not run(). -var shouldRunNow = true; +var wasmExports; -if (Module['noInitialRun']) shouldRunNow = false; +// With async instantation wasmExports is assigned asynchronously when the +// instance is received. +createWasm(); run(); - // end include: postamble.js + diff --git a/tools/assemble/ld65.wasm b/tools/assemble/ld65.wasm index 0476c809..0ed5e455 100755 Binary files a/tools/assemble/ld65.wasm and b/tools/assemble/ld65.wasm differ