data: imageLiteralToBitmap()

in share/src/components/GameModder.tsx [502:781]


                        data: imageLiteralToBitmap(blank),
                        name: name,
                        callToAction: CALL_TO_ACTION[name],
                        default: textToBitmap(def)
                    };
                })

            this.state = {
                userImages: imgs,
                currentImg: 0,
                currentBackground: 12
            }
            Object.assign(gameModderState, this.state)
        }

        this.tabImages = Object.keys(moddableImages)
            .map(k => moddableImages[k])
            .map(textToBitmap)

        if (!(gameModderState as GameModderState).alertShown) this.alertTimeout = setTimeout(this.alertPlay, 5000);
    }

    // async renderExperiments() {
    //     let tabBar = this.refs["tab-bar"] as TabBar
    //     let dummyImg = createPngImg(20, 20, 64, 64)
    //     tabBar.TabBarSvg.appendChild(dummyImg)
    //     setInterval(() => {
    //         updatePngImg(dummyImg, this.spriteEditor.bitmap().image)
    //     }, 500)

    //     function getImages(ts: string) {
    //         let imgRegex = /img`([\d\s\.a-f]*)`/gm
    //         let match = imgRegex.exec(ts);
    //         let res: string[] = []
    //         while (match != null) {
    //             res.push(match[1])
    //             match = imgRegex.exec(ts);
    //         }
    //         return res
    //     }

    //     // HACK:
    //     let mainTs = bunny_hop_main_ts;
    //     // let mainTs = await getTxtFile("games/bunny_hop/main.ts")

    //     // TODO: find images
    //     let imgs = getImages(mainTs)
    //     // console.dir(imgs)

    //     let imgsAsBmps = imgs.map(textToBitmap)
    //     // console.dir(imgsAsBmps)
    // }

    private alertPlay = () => {
        this.save();
        (gameModderState as GameModderState).alertShown = true;
        this.setState({ pulse: true });
    }

    private clearTimers = () => {
        clearTimeout(this.alertTimeout);
    }

    private updateCurrentUserImage(bmp: Bitmap) {
        // TODO: set image bug somehow?
        function updateUserImage(old: UserImage, nw: Bitmap): UserImage {
            return {
                data: nw,
                name: old.name,
                callToAction: old.callToAction,
                default: old.default
            }
        }
        let newState = {
            userImages: this.state.userImages.map((m, i) =>
                i === this.state.currentImg
                    ? updateUserImage(m, bmp)
                    : m)
        }
        this.setState(newState)
        Object.assign(gameModderState, newState)
    }

    private save() {
        if (this.spriteEditor && this.spriteEditor.editor) {
            this.spriteEditor.editor.commit()
            let newImg = this.spriteEditor.editor.bitmap().image
            this.updateCurrentUserImage(newImg)
        }
    }

    onTabChange(idx: number) {
        this.save()
        this.setState({ currentImg: idx })
        if (IsGameModderState(gameModderState))
            gameModderState.currentImg = idx
        tickEvent("shareExperiment.mod.tabChange", { "tab": idx });
    }

    onBackgroundColorChanged(idx: number) {
        this.setState({ currentBackground: idx })
        tickEvent("shareExperiment.mod.changeBackground", { "color": idx });
        if (IsGameModderState(gameModderState))
            gameModderState.currentBackground = idx
    }

    onSpriteGalleryPick(bmp: Bitmap, idx?: number) {
        tickEvent("shareExperiment.mod.galleryPick", { "tab": this.state.currentImg, "item": idx });
        this.updateCurrentUserImage(bmp)
    }

    render() {
        let currImg = this.state.userImages[this.state.currentImg]
        let isBackgroundTab = this.state.currentImg === 3

        let body = document.getElementsByTagName('body')[0]
        // const MARGIN = 20
        const HEADER_HEIGHT = 50
        let actualWidth = body.clientWidth
        let actualHeight = body.clientHeight - HEADER_HEIGHT
        let refWidth = 539.0
        let refHeight = SE.TOTAL_HEIGHT
        let wScale = actualWidth / refWidth
        let hScale = actualHeight / refHeight
        this.scale = Math.min(wScale, hScale)

        const SPRITE_GALLERY_HEIGHT = 100
        let spriteGalleryHeight = SPRITE_GALLERY_HEIGHT * this.scale
        let colorPickerHeight = (SE.TOTAL_HEIGHT + SPRITE_GALLERY_HEIGHT) * this.scale

        // TODO
        let samples = [
            SAMPLE_CHARACTERS,
            SAMPLE_OBSTACLES,
            SAMPLE_OBSTACLES2
        ]
        let spriteGalleryOptions =
            (samples[this.state.currentImg] || SAMPLE_CHARACTERS)
                .map(i => imageLiteralToBitmap(i))

        let startImg = this.state.userImages[this.state.currentImg].data
        let galKey = `tab${this.state.currentImg}__` + spriteGalleryOptions.map(b => b.buf.toString()).join("_")
        let galProps: SpriteGalleryProps = {
            height: spriteGalleryHeight,
            options: spriteGalleryOptions,
            onClick: this.onSpriteGalleryPick.bind(this)
        }
        return (
            <div className="game-modder">
                <h1 ref="header" className="what-to-do-header">{currImg.callToAction}</h1>
                <TabBar ref="tab-bar" tabImages={this.tabImages}
                    tabChange={this.onTabChange.bind(this)} startTab={this.state.currentImg} />
                {isBackgroundTab
                    ?
                    <ColorPicker selectionChanged={this.onBackgroundColorChanged.bind(this)}
                        selected={this.state.currentBackground} colors={SE.COLORS}
                        height={colorPickerHeight}></ColorPicker>
                    :
                    <SpriteEditorComp ref="sprite-editor" startImage={startImg}
                        onPlay={this.onPlay} scale={this.scale} galleryProps={galProps}></SpriteEditorComp>
                }
                {/* <div ref="sprite-gallery" className="sprite-gallery">
                </div> */}
                <button ref="play-btn" className={`play-btn ${this.state.pulse ? "shake" : ""}`}>
                    <span>Play</span>
                    <i className="icon play"></i>
                </button>
            </div>
        )
    }

    async componentDidMount() {
        this.playBtn = this.refs["play-btn"] as HTMLButtonElement;
        this.spriteEditor = this.refs["sprite-editor"] as SpriteEditorComp;
        this.header = this.refs['header'] as HTMLHeadingElement

        // events
        this.playBtn.addEventListener('click', this.onPlay.bind(this))

        // HACK: Disable scrolling in iOS
        document.ontouchmove = function (e) {
            e.preventDefault();
        }
    }

    componentDidUpdate() {
        this.spriteEditor = this.refs["sprite-editor"] as SpriteEditorComp;
    }

    componentWillUnmount() {
        this.playBtn = undefined;
        this.spriteEditor = undefined;
        this.header = undefined;

        this.clearTimers();
    }

    async onPlay() {
        this.save();
        (gameModderState as GameModderState).alertShown = true;

        const toReplace = this.state.userImages.filter(ui => !isEmptyBitmap(ui.data));

        function modBackground(bin: string, newColor: number): string {
            const originalColor = 13
            const template = (color: number) => `scene_setBackgroundColor__P935_mk(s);s.tmp_0.arg0=${color}`
            let old = template(originalColor)
            let newIdx = newColor + 1 // arcade function is 1-based b/c 0 is transparent
            let nw = template(newIdx)
            return bin.replace(old, nw)
        }

        function modBackgroundTs(bin: string, newColor: number): string {
            const originalColor = 13
            const template = (color: number) => `scene.setBackgroundColor(${color})`
            let old = template(originalColor)
            let newIdx = newColor + 1 // arcade function is 1-based b/c 0 is transparent
            let nw = template(newIdx)
            return bin.replace(old, nw)
        }

        function modImg(bin: string, img: UserImage): string {
            // HACK: for some reason the compiler emits image prefixes that look like:
            // 8704100010000000
            // whereas ours look like:
            // e4101000
            const MOD_PREFIX_LEN = "e4101000".length
            const BIN_PREFIX_LEN = "8704100010000000".length

            let newHex = bitmapToBinHex(img.data)

            const oldToFind = bitmapToBinHex(img.default)
                .slice(MOD_PREFIX_LEN)
            let oldStartIncl = bin.indexOf(oldToFind) - BIN_PREFIX_LEN
            if (oldStartIncl < 0)
                return bin;
            let oldEndExcl = bin.indexOf(`"`, oldStartIncl)
            let oldHex = bin.slice(oldStartIncl, oldEndExcl)

            return bin.replace(oldHex, newHex)
        }

        let gameBinJs = bunny_hop_bin_js
        let gameMainTs = bunny_hop_main_ts
        let gameMainBlocks = bunny_hop_main_blocks;

        for (let i of toReplace) {
            const def = bitmapToText(i.default);
            const user = bitmapToText(i.data);
            gameBinJs = modImg(gameBinJs, i)
            gameMainTs = replaceImages(gameMainTs, def, user);
            gameMainBlocks = replaceImages(gameMainBlocks, def, user);
        }
        gameBinJs = modBackground(gameBinJs, this.state.currentBackground)
        gameMainTs = modBackgroundTs(gameMainTs, this.state.currentBackground);

        const screenshot = await mkScreenshotAsync(this.state.currentBackground + 1, this.state.userImages.map(u => isEmptyBitmap(u.data) ? u.default : u.data));

        this.props.playHandler({
            binJs: gameBinJs,
            mainTs: gameMainTs,
            mainBlocks: gameMainBlocks,
            screenshot
        });
    }
}

function replaceImages(sourceFile: string, toReplace: string, userImage: string) {
    const sourceLines = sourceFile.split(/\n/).map(l => l.trim());
    const replaceLines = toReplace.split(/\n/).map(l => l.trim()).slice(1, -1);

    userImage = userImage.replace("img`", "").replace("`", "");

    let foundMatch = false;
    for (let i = 0; i < sourceLines.length; i++) {
        if (sourceLines[i] === replaceLines[0]) {
            foundMatch = true;

            for (let j = 1; j < replaceLines.length; j++) {
                if (sourceLines[i + j] != replaceLines[j]) {