[
  {
    "path": ".editorconfig",
    "content": "# see https://github.com/CppCXY/EmmyLuaCodeStyle\n[*.lua]\n\nmax_line_length = 120\nend_of_line = lf\nindent_style = tab\nindent_size = 4\nquote_style = double\ncall_arg_parentheses = remove\nauto_collapse_lines = false\n"
  },
  {
    "path": ".github/actions/sample/action.yml",
    "content": "name: Build Sample WASM\ndescription: Build sample.wasm side module for Soluna web runtime\n\ninputs:\n  soluna_path:\n    description: \"The path to the Soluna repository. Defaults to .\"\n    required: false\n    default: \".\"\n  luamake_version:\n    description: \"LuaMake commit SHA used to build.\"\n    required: false\n    default: \"5bedfce66f075a9f68b1475747738b81b3b41c25\"\n  emsdk_version:\n    description: \"Emscripten SDK version.\"\n    required: false\n    default: \"4.0.17\"\n\noutputs:\n  SAMPLE_WASM_PATH:\n    description: \"The path to built sample.wasm side module.\"\n    value: ${{ steps.set-output.outputs.SAMPLE_WASM_PATH }}\n\nruns:\n  using: \"composite\"\n  steps:\n    - name: Get the soluna commit\n      id: refs\n      working-directory: ${{ inputs.soluna_path }}\n      shell: bash\n      run: |\n        echo \"commit=$(git rev-parse HEAD)\" >> \"$GITHUB_OUTPUT\"\n\n    - name: Cache sample wasm build\n      uses: actions/cache@v4\n      id: cache\n      with:\n        path: |\n          ${{ inputs.soluna_path }}/bin\n          ${{ inputs.soluna_path }}/build\n        key: ${{ runner.os }}-sample-wasm-${{ steps.refs.outputs.commit }}-${{ inputs.luamake_version }}-${{ inputs.emsdk_version }}\n\n    - name: Checkout all submodules\n      if: steps.cache.outputs.cache-hit != 'true'\n      working-directory: ${{ inputs.soluna_path }}\n      shell: bash\n      run: |\n        git submodule update --init --recursive\n\n    - name: Setup LuaMake\n      if: steps.cache.outputs.cache-hit != 'true'\n      uses: yuchanns/actions-luamake@v1\n      with:\n        luamake-version: ${{ inputs.luamake_version }}\n\n    - name: Setup Emscripten\n      if: runner.os == 'Linux' && steps.cache.outputs.cache-hit != 'true'\n      uses: mymindstorm/setup-emsdk@v14\n      with:\n        version: ${{ inputs.emsdk_version }}\n        actions-cache-folder: 'emsdk-cache'\n\n    - name: Build sample side module\n      if: runner.os == 'Linux' && steps.cache.outputs.cache-hit != 'true'\n      shell: bash\n      working-directory: ${{ inputs.soluna_path }}\n      run: |\n        luamake -compiler emcc sample\n\n    - name: Set output\n      id: set-output\n      shell: bash\n      working-directory: ${{ inputs.soluna_path }}\n      run: |\n        soluna_root=$(pwd -P)\n        SAMPLE_WASM_PATH=$(find \"$soluna_root/bin\" -name \"sample.wasm\" | head -n 1)\n        if [ -z \"$SAMPLE_WASM_PATH\" ]; then\n          echo \"sample.wasm not found\" >&2\n          exit 1\n        fi\n        echo \"SAMPLE_WASM_PATH=$SAMPLE_WASM_PATH\" >> \"$GITHUB_OUTPUT\"\n"
  },
  {
    "path": ".github/actions/soluna/action.yml",
    "content": "name: Build Soluna\ndescription: Build Soluna for different operating systems\n\ninputs:\n  soluna_path:\n    description: 'The path to the Soluna repository. Defaults to .'\n    required: false\n    default: '.'\n  debug:\n    description: 'Whether to build Soluna in debug mode. Defaults to false.'\n    required: false\n    default: 'false'\n\noutputs:\n  SOLUNA_BINARY:\n    description: 'The name of the built Soluna binary.'\n    value: ${{ steps.set-output.outputs.SOLUNA_BINARY }}\n  SOLUNA_PATH:\n    description: 'The path to the built Soluna binary.'\n    value: ${{ steps.set-output.outputs.SOLUNA_PATH }}\n  SOLUNA_WASM_PATH:\n    description: 'The path to the built Soluna WebAssembly binary.'\n    value: ${{ steps.set-output.outputs.SOLUNA_WASM_PATH }}\n  SOLUNA_JS_PATH:\n    description: 'The path to the Soluna JavaScript glue code for WebAssembly.'\n    value: ${{ steps.set-output.outputs.SOLUNA_JS_PATH }}\n  SOLUNA_WASM_MAP_PATH:\n    description: 'The path to the built Soluna WebAssembly map if debug is true.'\n    value: ${{ steps.set-output.outputs.SOLUNA_WASM_MAP_PATH }}\n\nruns:\n  using: \"composite\"\n  steps:\n    - name: Get the soluna commit\n      id: refs\n      working-directory: ${{ inputs.soluna_path }}\n      run: |\n        echo \"commit=$(git rev-parse HEAD)\" >> $GITHUB_OUTPUT\n      shell: bash\n    - name: Cache soluna build\n      uses: actions/cache@v4\n      id: cache\n      with:\n        path: ${{ inputs.soluna_path }}/bin\n        key: ${{ runner.os }}-soluna-build-${{ steps.refs.outputs.commit }}-${{ inputs.debug }}\n    - name: Checkout all submodules\n      if: steps.cache.outputs.cache-hit != 'true'\n      working-directory: ${{ inputs.soluna_path }}\n      run: |\n        git submodule update --init --recursive\n      shell: bash\n    - uses: yuchanns/actions-luamake@v1\n      if: steps.cache.outputs.cache-hit != 'true'\n      with:\n        luamake-version: \"5bedfce66f075a9f68b1475747738b81b3b41c25\"\n    - name: Install Dependencies (Linux)\n      shell: bash\n      if: runner.os == 'Linux' && steps.cache.outputs.cache-hit != 'true'\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y build-essential \\\n          libgl1-mesa-dev libglu1-mesa-dev libx11-dev \\\n          libxrandr-dev libxi-dev libxxf86vm-dev libxcursor-dev \\\n          libasound2-dev libfontconfig1-dev\n    - name: Build (Windows)\n      if: runner.os == 'Windows' && steps.cache.outputs.cache-hit != 'true'\n      shell: powershell\n      working-directory: ${{ inputs.soluna_path }}\n      run: |\n        if (${{ inputs.debug }} -eq 'true') {\n          $env:LUAMAKE_BUILD_TYPE = 'debug'\n        } else {\n          $env:LUAMAKE_BUILD_TYPE = 'release'\n        }\n        luamake -mode $env:LUAMAKE_BUILD_TYPE soluna\n    - name: Build (Unix)\n      if: runner.os != 'Windows' && steps.cache.outputs.cache-hit != 'true'\n      shell: bash\n      working-directory: ${{ inputs.soluna_path }}\n      run: |\n        if [ \"${{ inputs.debug }}\" == \"true\" ]; then\n          export LUAMAKE_BUILD_TYPE=debug\n        else\n          export LUAMAKE_BUILD_TYPE=release\n        fi\n        luamake -mode $LUAMAKE_BUILD_TYPE soluna\n    - uses: mymindstorm/setup-emsdk@v14\n      if: runner.os == 'Linux' && steps.cache.outputs.cache-hit != 'true'\n      with:\n        version: 4.0.17\n        actions-cache-folder: 'emsdk-cache'\n    - name: Build Emscripten\n      if: runner.os == 'Linux' && steps.cache.outputs.cache-hit != 'true'\n      working-directory: ${{ inputs.soluna_path }}\n      shell: bash\n      run: |\n        if [ \"${{ inputs.debug }}\" == \"true\" ]; then\n          export LUAMAKE_BUILD_TYPE=debug\n        else\n          export LUAMAKE_BUILD_TYPE=release\n        fi\n        luamake -mode $LUAMAKE_BUILD_TYPE -compiler emcc\n        SOLUNA_JS=\"soluna.js\"\n        SOLUNA_JS_PATH=$(find bin -name $SOLUNA_JS | head -n 1)\n        sed -i 's/setBindGroup(groupIndex,group,(growMemViews(),HEAPU32),dynamicOffsetsPtr>>2,dynamicOffsetCount)/setBindGroup(groupIndex,group,(growMemViews(),HEAPU32).subarray(dynamicOffsetsPtr>>2,(dynamicOffsetsPtr>>2)+dynamicOffsetCount))/g' \"$SOLUNA_JS_PATH\"\n        sed -i 's/setBindGroup(groupIndex, group, (growMemViews(), HEAPU32), ((dynamicOffsetsPtr) >> 2), dynamicOffsetCount)/setBindGroup(groupIndex, group, (growMemViews(), HEAPU32).subarray(((dynamicOffsetsPtr) >> 2), ((dynamicOffsetsPtr) >> 2) + dynamicOffsetCount))/g' \"$SOLUNA_JS_PATH\"\n        perl -0pi -e 's/var group=WebGPU\\.getJsObject\\(groupPtr\\);if\\(dynamicOffsetCount==0\\)\\{pass\\.setBindGroup\\(groupIndex,group\\)\\}else\\{pass\\.setBindGroup\\(groupIndex,group,\\(growMemViews\\(\\),HEAPU32\\)\\.subarray\\(dynamicOffsetsPtr>>2,\\(dynamicOffsetsPtr>>2\\)\\+dynamicOffsetCount\\)\\)\\}/var group=WebGPU.getJsObject(groupPtr);if(!group){return}if(dynamicOffsetCount==0){pass.setBindGroup(groupIndex,group)}else{pass.setBindGroup(groupIndex,group,(growMemViews(),HEAPU32).subarray(dynamicOffsetsPtr>>2,(dynamicOffsetsPtr>>2)+dynamicOffsetCount))}/g' \"$SOLUNA_JS_PATH\"\n        perl -0pi -e 's/var group = WebGPU\\.getJsObject\\(groupPtr\\);\\n  if \\(dynamicOffsetCount == 0\\) \\{\\n    pass\\.setBindGroup\\(groupIndex, group\\);\\n  \\} else \\{\\n    pass\\.setBindGroup\\(groupIndex, group, \\(growMemViews\\(\\), HEAPU32\\)\\.subarray\\(\\(\\(dynamicOffsetsPtr\\) >> 2\\), \\(\\(dynamicOffsetsPtr\\) >> 2\\) \\+ dynamicOffsetCount\\)\\);\\n  \\}/var group = WebGPU.getJsObject(groupPtr);\\n  if (!group) {\\n    return;\\n  }\\n  if (dynamicOffsetCount == 0) {\\n    pass.setBindGroup(groupIndex, group);\\n  } else {\\n    pass.setBindGroup(groupIndex, group, (growMemViews(), HEAPU32).subarray(((dynamicOffsetsPtr) >> 2), ((dynamicOffsetsPtr) >> 2) + dynamicOffsetCount));\\n  }/g' \"$SOLUNA_JS_PATH\"\n        if grep -Fq 'setBindGroup(groupIndex,group,(growMemViews(),HEAPU32),dynamicOffsetsPtr>>2,dynamicOffsetCount)' \"$SOLUNA_JS_PATH\" || \\\n           grep -Fq 'setBindGroup(groupIndex, group, (growMemViews(), HEAPU32), ((dynamicOffsetsPtr) >> 2), dynamicOffsetCount)' \"$SOLUNA_JS_PATH\"; then\n          echo \"Unpatched WebGPU setBindGroup glue remains in $SOLUNA_JS_PATH\" >&2\n          exit 1\n        fi\n    - name: Set Output Build Path\n      id: set-output\n      run: |\n        soluna_root=$(cd \"${{ inputs.soluna_path }}\" && pwd -P)\n        if [ \"${{ runner.os }}\" == \"Windows\" ]; then\n          soluna_root_output=$(cd \"${{ inputs.soluna_path }}\" && pwd -W | sed 's#\\\\#/#g')\n        else\n          soluna_root_output=\"$soluna_root\"\n        fi\n        bin_dir=\"$soluna_root/bin\"\n        bin_dir_output=\"$soluna_root_output/bin\"\n        if [ \"${{ runner.os }}\" == \"Windows\" ]; then\n          SOLUNA_BINARY=\"soluna.exe\"\n          RENAME_BINARY=\"soluna-windows-amd64.exe\"\n        elif [ \"${{ runner.os }}\" == \"macOS\" ]; then\n          SOLUNA_BINARY=\"soluna\"\n          RENAME_BINARY=\"soluna-macos-arm64\"\n        else\n          SOLUNA_BINARY=\"soluna\"\n          RENAME_BINARY=\"soluna-linux-amd64\"\n        fi\n        SOLUNA_PATH=$(find \"$bin_dir\" -name \"$SOLUNA_BINARY\" | head -n 1)\n        cp \"$SOLUNA_PATH\" \"$bin_dir/$RENAME_BINARY\"\n        echo \"SOLUNA_PATH=$bin_dir_output/$RENAME_BINARY\" >> $GITHUB_OUTPUT\n        echo \"SOLUNA_BINARY=$RENAME_BINARY\" >> $GITHUB_OUTPUT\n        if [ \"${{ runner.os }}\" == \"Linux\" ]; then\n          SOLUNA_WASM_PATH=$(find \"$bin_dir\" -name \"soluna.wasm\" | head -n 1)\n          SOLUNA_JS_PATH=$(find \"$bin_dir\" -name \"soluna.js\" | head -n 1)\n          echo \"SOLUNA_WASM_PATH=$SOLUNA_WASM_PATH\" >> $GITHUB_OUTPUT\n          echo \"SOLUNA_JS_PATH=$SOLUNA_JS_PATH\" >> $GITHUB_OUTPUT\n          if [ \"${{ inputs.debug }}\" == \"true\" ]; then\n            SOLUNA_WASM_MAP_PATH=$(find \"$bin_dir\" -name \"soluna.wasm.map\" | head -n 1)\n            echo \"SOLUNA_WASM_MAP_PATH=$SOLUNA_WASM_MAP_PATH\" >> $GITHUB_OUTPUT\n          fi\n        fi\n      shell: bash\n"
  },
  {
    "path": ".github/workflows/nightly.yml",
    "content": "name: Nightly Build\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    branches:\n      - master\n  workflow_dispatch:\n  schedule:\n    - cron: '0 0 * * *'  # Runs every day at midnight UTC\n\njobs:\n  check:\n    name: Determine Build Necessity\n    runs-on: ubuntu-latest\n    outputs:\n      proceed: ${{ steps.check.outputs.proceed }}\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n      - name: Get the last nightly commit\n        id: last_nightly\n        uses: actions/github-script@v7\n        with:\n          script: |\n            const releases = await github.rest.repos.listReleases({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              per_page: 100\n            });\n            const nightlyRelease = releases.data.find(release => release.tag_name.includes('nightly') || release.name.includes('Nightly'));\n            if (nightlyRelease) {\n              const tagName = nightlyRelease.tag_name;\n              const tag = await github.rest.git.getRef({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                ref: `tags/${tagName}`\n              });\n              const commitSha = tag.data.object.sha;\n              return commitSha;\n            } else {\n              return null;\n            }\n      - name: Check the proceed condition\n        id: check\n        run: |\n          LAST_NIGHTLY_COMMIT=\"${{ steps.last_nightly.outputs.result }}\"\n          CURRENT_COMMIT=\"${GITHUB_SHA}\"\n          echo \"Last nightly commit: $LAST_NIGHTLY_COMMIT\"\n          echo \"Current commit: $CURRENT_COMMIT\"\n          if [ -z \"$LAST_NIGHTLY_COMMIT\" ]; then\n            echo \"No previous nightly release found. Proceeding with build.\"\n            echo \"proceed=true\" >> $GITHUB_OUTPUT\n          elif [ \"$LAST_NIGHTLY_COMMIT\" != \"$CURRENT_COMMIT\" ]; then\n            echo \"New commits found since last nightly release. Proceeding with build.\"\n            echo \"proceed=true\" >> $GITHUB_OUTPUT\n          else\n            echo \"No new commits since last nightly release. Skipping build.\"\n            echo \"proceed=false\" >> $GITHUB_OUTPUT\n          fi\n  build:\n    name: Nightly Build on ${{ matrix.os }}\n    needs: check\n    if: needs.check.outputs.proceed == 'true'\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [windows-latest, macos-latest, ubuntu-latest]\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          submodules: recursive\n      - uses: ./.github/actions/soluna\n        name: Build\n        id: build\n        with:\n          soluna_path: \".\"\n      - uses: actions/upload-artifact@v4\n        name: Upload\n        if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'\n        with:\n          name: \"soluna-${{ runner.os }}-${{ steps.build.outputs.SOLUNA_BINARY }}\"\n          if-no-files-found: \"error\"\n          path: \"${{ steps.build.outputs.SOLUNA_PATH }}\"\n          overwrite: \"true\"\n      - uses: actions/upload-artifact@v4\n        name: Upload Emscripten\n        if: (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') && runner.os == 'Linux'\n        with:\n          name: \"soluna-emscripten\"\n          if-no-files-found: \"error\"\n          overwrite: \"true\"\n          path: |\n            ${{ steps.build.outputs.SOLUNA_WASM_PATH }}\n            ${{ steps.build.outputs.SOLUNA_JS_PATH }}\n  release:\n    name: Create Nightly Release\n    needs: [check, build]\n    runs-on: ubuntu-latest\n    if: (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') && github.actor != 'nektos/act'\n    permissions:\n      contents: write\n    steps:\n      - uses: actions/checkout@v6\n      - name: Download all artifacts\n        uses: actions/download-artifact@v5\n        with:\n          path: artifacts\n      - name: Prepare release assets\n        run: |\n          mkdir -p release-assets\n          find artifacts -type f -name \"soluna*\" -exec cp {} release-assets/ \\;\n          ls -la release-assets/\n      - name: Delete existing nightly releases\n        uses: actions/github-script@v7\n        with:\n          script: |\n            const releases = await github.rest.repos.listReleases({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              per_page: 100\n            });\n\n            for (const release of releases.data) {\n              if (release.tag_name.includes('nightly') || release.name.includes('Nightly')) {\n                console.log(`Deleting release: ${release.tag_name}`);\n                try {\n                  await github.rest.repos.deleteRelease({\n                    owner: context.repo.owner,\n                    repo: context.repo.repo,\n                    release_id: release.id\n                  });\n\n                  try {\n                    await github.rest.git.deleteRef({\n                      owner: context.repo.owner,\n                      repo: context.repo.repo,\n                      ref: `tags/${release.tag_name}`\n                    });\n                  } catch (error) {\n                    console.log(`Tag ${release.tag_name} might not exist or already deleted`);\n                  }\n                } catch (error) {\n                  console.log(`Failed to delete release ${release.tag_name}: ${error.message}`);\n                }\n              }\n            }\n      - name: Create nightly release\n        id: create-release\n        uses: actions/github-script@v7\n        with:\n          script: |\n            const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);\n            const commitSha = context.sha.substring(0, 7);\n            const tagName = 'nightly';\n            const releaseNotes = `🌙 **Nightly Build**\n\n            **Build Information:**\n            - **Commit:** \\`${context.sha}\\`\n            - **Branch:** \\`${context.ref.replace('refs/heads/', '')}\\`\n            - **Build Time:** \\`${new Date().toISOString()}\\`\n            - **Workflow:** [${context.runId}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})\n\n            > ⚠️ **Note:** This is an automated nightly build. It may contain unstable features and bugs.\n            > \n            > 📦 **Previous nightly releases are automatically removed to keep the repository clean.**`;\n            \n            const { data } = await github.rest.repos.createRelease({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              tag_name: tagName,\n              name: `Nightly Build (${timestamp})`,\n              body: releaseNotes,\n              prerelease: true,\n              make_latest: 'true',\n              draft: false,\n            });\n            \n            console.log(`Created release: ${data.html_url}`);\n            return data.id;\n      - name: Upload release assets\n        uses: actions/github-script@v7\n        with:\n          script: |\n            const fs = require('fs');\n            const path = require('path');\n            const releaseId = ${{ steps.create-release.outputs.result }};\n            \n            const assetsDir = 'release-assets';\n            const files = fs.readdirSync(assetsDir);\n            \n            for (const file of files) {\n              const filePath = path.join(assetsDir, file);\n              const stats = fs.statSync(filePath);\n              \n              if (stats.isFile()) {\n                console.log(`Uploading ${file}...`);\n                \n                const content = fs.readFileSync(filePath);\n                \n                await github.rest.repos.uploadReleaseAsset({\n                  owner: context.repo.owner,\n                  repo: context.repo.repo,\n                  release_id: releaseId,\n                  name: file,\n                  data: content\n                });\n                \n                console.log(`Uploaded ${file}`);\n              }\n            }\n"
  },
  {
    "path": ".github/workflows/pages.yml",
    "content": "name: GitHub Pages\n\non:\n  workflow_run:\n    workflows:\n      - Nightly Build\n    types:\n      - completed\n    branches:\n      - master\n  workflow_dispatch:\n    inputs:\n      debug:\n        description: 'Whether to build in debug mode'\n        required: false\n        default: 'false'\n\njobs:\n  build:\n    name: Build Web Site\n    if: |\n      github.event_name == 'workflow_dispatch' ||\n      (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')\n    runs-on: ubuntu-latest\n    concurrency:\n      group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.ref }}\n    permissions:\n      contents: read\n      pages: write\n      id-token: write\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          submodules: recursive\n          fetch-depth: 0\n          ref: ${{ github.event.workflow_run.head_sha || github.sha }}\n\n      - name: Build Soluna\n        uses: ./.github/actions/soluna\n        id: build\n        with:\n          soluna_path: \".\"\n          debug: ${{ github.event.inputs.debug }}\n\n      - name: Build Sample\n        uses: ./.github/actions/sample\n        id: sample\n        with:\n          soluna_path: \".\"\n\n      - name: Build Astro\n        uses: withastro/action@v6\n        env:\n          SITE_BASE: /soluna/\n          SOLUNA_JS_PATH: ${{ steps.build.outputs.SOLUNA_JS_PATH }}\n          SOLUNA_WASM_PATH: ${{ steps.build.outputs.SOLUNA_WASM_PATH }}\n          SOLUNA_WASM_MAP_PATH: ${{ steps.build.outputs.SOLUNA_WASM_MAP_PATH }}\n          SAMPLE_WASM_PATH: ${{ steps.sample.outputs.SAMPLE_WASM_PATH }}\n        with:\n          path: website\n          node-version: 24\n          package-manager: pnpm@10.28.2\n          build-cmd: pnpm run build\n          out-dir: dist\n\n  deploy:\n    name: Deploy to GitHub Pages\n    if: |\n      (github.event_name == 'workflow_dispatch' ||\n      (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')) &&\n      github.actor != 'nektos/act'\n    needs: [build]\n    runs-on: ubuntu-latest\n    permissions:\n      pages: write\n      id-token: write\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".gitignore",
    "content": "bin\nbuild\ncompile_commands.json\n.cache\nnode_modules\n.pnpm-store\npnpm-debug.log*\nwebsite/node_modules\nwebsite/packages/*/node_modules\nwebsite/dist\nwebsite/.astro\nwebsite/public/runtime/\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"3rd/ltask\"]\n\tpath = 3rd/ltask\n\turl = https://github.com/cloudwu/ltask.git\n[submodule \"3rd/sokol\"]\n\tpath = 3rd/sokol\n\turl = https://github.com/floooh/sokol.git\n[submodule \"3rd/lua\"]\n\tpath = 3rd/lua\n\turl = https://github.com/lua/lua.git\n[submodule \"3rd/stb\"]\n\tpath = 3rd/stb\n\turl = https://github.com/nothings/stb.git\n[submodule \"3rd/datalist\"]\n\tpath = 3rd/datalist\n\turl = https://github.com/cloudwu/datalist.git\n[submodule \"3rd/yoga\"]\n\tpath = 3rd/yoga\n\turl = https://github.com/facebook/yoga.git\n[submodule \"3rd/zlib\"]\n\tpath = 3rd/zlib\n\turl = https://github.com/madler/zlib.git\n[submodule \"bin/sokol-tools-bin\"]\n\tpath = bin/sokol-tools-bin\n\turl = https://github.com/floooh/sokol-tools-bin.git\n[submodule \"3rd/miniaudio\"]\n\tpath = 3rd/miniaudio\n\turl = https://github.com/mackron/miniaudio.git\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2025 codingnow.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY : all clean shader extlua_sample\n\nBUILD=build\nBIN=bin\nAPPNAME=soluna.exe\nCC?=gcc\n# msvc support\n#CC=cl\nLD=$(CC)\nLUA_EXE=$(BUILD)/lua.exe\nSHDC=$(BIN)/sokol-tools-bin/bin/win32/sokol-shdc.exe\nVERSION=$(shell git rev-parse HEAD)\n\n#for msvc\nifeq ($(CC),cl)\n CCPP=cl\n CFLAGS=-utf-8 -W3 -O2\n OUTPUT_O=-c -Fo:\n OUTPUT_EXE=-Fe:\n STDC=-std:c11 -experimental:c11atomics\n STDCPP=-std:c++20\n SUBSYSTEM=-LINK -SUBSYSTEM:WINDOWS -ENTRY:\"mainCRTStartup\"\n LDFLAGS=$(SUBSYSTEM) xinput.lib Ws2_32.lib ntdll.lib Imm32.lib\n SHARED=-LD\nelse\n CCPP=g++\n CFLAGS=-Wall -O2\n OUTPUT_O=-c -o\n OUTPUT_EXE=-o\n STDC=-std=c99 -lm\n STDCPP=-std=c++20\n SUBSYSTEM=-Wl,-subsystem,windows\n LDFLAGS=-lkernel32 -luser32 -lshell32 -lgdi32 -ldxgi -ld3d11 -lwinmm -lws2_32 -lntdll -lxinput -limm32 -lstdc++ $(SUBSYSTEM)\n SHARED=--shared\nendif\n\nall : $(BIN)/$(APPNAME)\n\n3RDINC=-I3rd\nYOGAINC=-I3rd/yoga\nMINIAUDIOINC=-I3rd/miniaudio\n\nLUAINC=-I3rd/lua\nLUASRC:=$(wildcard 3rd/lua/*.c 3rd/lua/*.h)\nWINFILE:=src/winfile.c\n\n$(LUA_EXE) : $(LUASRC) $(WINFILE)\n\t$(CC) $(CFLAGS) -o $@ 3rd/lua/onelua.c $(WINFILE) -DMAKE_LUA -Dfopen=fopen_utf8\n\nCOMPILE_C=$(CC) $(CFLAGS) $(STDC) $(OUTPUT_O) $@ $<\nCOMPILE_LUA=$(LUA_EXE) script/lua2c.lua $< $@\nCOMPILE_DATALIST=$(LUA_EXE) script/datalist2c.lua $< $@\n\nLUA_O=$(BUILD)/onelua.o\n\n$(LUA_O) : $(LUASRC)\n\t$(CC) $(CFLAGS) $(OUTPUT_O) $@ 3rd/lua/onelua.c -DMAKE_LIB -Dfopen=fopen_utf8\n\nSHADER_SRC=$(wildcard src/*.glsl)\nSHADER_O=$(patsubst src/%.glsl,$(BUILD)/%.glsl.h,$(SHADER_SRC))\nEXTLUA_SHADER_SRC=$(wildcard extlua/*.glsl)\nEXTLUA_SHADER_O=$(patsubst extlua/%.glsl,$(BUILD)/%.glsl.h,$(EXTLUA_SHADER_SRC))\nSHADERINC=-I$(BUILD)\n\n$(BUILD)/%.glsl.h : src/%.glsl\n\t$(SHDC) --input $< --output $@ --slang hlsl4 --format sokol\n\n$(BUILD)/%.glsl.h : extlua/%.glsl\n\t$(SHDC) --input $< --output $@ --slang hlsl4 --format sokol\n\nshader : $(SHADER_O) $(EXTLUA_SHADER_O)\n\nMAIN_FULL=$(wildcard src/*.c)\nPLATFORM_FULL=$(wildcard src/platform/windows/*.c)\nMAIN_C=$(notdir $(MAIN_FULL))\nMAIN_O=$(patsubst %.c,$(BUILD)/soluna_%.o,$(MAIN_C))\nPLATFORM_C=$(notdir $(PLATFORM_FULL))\nPLATFORM_O=$(patsubst %.c,$(BUILD)/platform_%.o,$(PLATFORM_C))\nEXTLUA_O=$(BUILD)/extlua_impl.o $(BUILD)/sokolapi_impl.o $(BUILD)/solunaapi_impl.o\n\n$(MAIN_O) : $(SHADER_O)\n\nLTASK_FULL=$(wildcard 3rd/ltask/src/*.c)\nLTASK_C=$(notdir $(LTASK_FULL))\nLTASK_O=$(patsubst %.c,$(BUILD)/ltask_%.o,$(LTASK_C))\n\nLTASK_LUASRC=\\\n  3rd/ltask/service/root.lua\\\n  3rd/ltask/service/timer.lua\\\n  $(wildcard 3rd/ltask/lualib/*.lua src/lualib/*.lua src/service/*.lua src/material/*.lua)\n\nLTASK_LUACODE=$(patsubst %.lua, $(BUILD)/%.lua.h, $(notdir $(LTASK_LUASRC)))\n\nDATALIST_SRC=$(wildcard src/data/*.dl)\n\nDATALIST_CODE=$(patsubst %.dl, $(BUILD)/%.dl.h, $(notdir $(DATALIST_SRC)))\n\nZLIBINC=-I3rd/zlib\nZLIB_FULL=$(wildcard 3rd/zlib/*.c)\nZLIB_C = $(notdir $(ZLIB_FULL))\nZLIB_O = $(patsubst %.c,$(BUILD)/zlib_%.o,$(ZLIB_C))\nMINIZIP_FULL=\\\n  3rd\\zlib\\contrib/minizip/ioapi.c\\\n  3rd\\zlib\\contrib/minizip/unzip.c\\\n  3rd\\zlib\\contrib/minizip/zip.c\\\n  3rd\\zlib\\contrib/minizip/iowin32.c\nMINIZIP_C = $(notdir $(MINIZIP_FULL))\nMINIZIP_O = $(patsubst %.c,$(BUILD)/minizip_%.o,$(MINIZIP_C))\n\n$(LTASK_LUACODE) $(DATALIST_CODE) : | $(LUA_EXE)\n\n$(BUILD)/%.lua.h : 3rd/ltask/service/%.lua\n\t$(COMPILE_LUA)\n\n$(BUILD)/%.lua.h : 3rd/ltask/lualib/%.lua\n\t$(COMPILE_LUA)\n\n$(BUILD)/%.lua.h : src/lualib/%.lua\n\t$(COMPILE_LUA)\n\n$(BUILD)/%.lua.h : src/service/%.lua\n\t$(COMPILE_LUA)\n\n$(BUILD)/%.lua.h : src/material/%.lua\n\t$(COMPILE_LUA)\n\n$(BUILD)/%.dl.h : src/data/%.dl\n\t$(COMPILE_DATALIST)\n\n$(BUILD)/soluna_embedlua.o : src/embedlua.c $(LTASK_LUACODE) $(DATALIST_CODE)\n\t$(COMPILE_C) -I$(BUILD) $(LUAINC)\n\n$(BUILD)/soluna_entry.o : src/entry.c src/version.h\n\t$(COMPILE_C) $(LUAINC) $(3RDINC) -DSOLUNA_HASH_VERSION=\\\"$(VERSION)\\\"\n\n$(BUILD)/soluna_%.o : src/%.c\n\t$(COMPILE_C) $(LUAINC) $(3RDINC) $(SHADERINC) $(YOGAINC) $(ZLIBINC) $(MINIAUDIOINC)\n\n$(BUILD)/platform_%.o : src/platform/windows/%.c\n\t$(COMPILE_C) $(LUAINC) $(3RDINC) $(SHADERINC) $(YOGAINC) $(ZLIBINC) -Isrc\n\n$(BUILD)/ltask_%.o : 3rd/ltask/src/%.c\n\t$(COMPILE_C) $(LUAINC) -D_WIN32_WINNT=0x0601 -DLTASK_EXTERNAL_OPENLIBS=soluna_openlibs\n\t\nDATALIST_O=$(BUILD)/datalist.o\n\n$(DATALIST_O) : 3rd/datalist/datalist.c\n\t$(COMPILE_C) $(LUAINC)\n\t\nYOGASRC:=$(wildcard 3rd/yoga/yoga/*.cpp $(addsuffix *.cpp,$(wildcard 3rd/yoga/yoga/*/)))\n\n$(BUILD)/yoga.o : src/yogaone.cpp $(YOGASRC)\n\t$(CCPP) $(STDCPP) $(OUTPUT_O) $@ $< $(YOGAINC) $(CFLAGS)\n\t\n$(BUILD)/zlib_%.o : 3rd/zlib/%.c\n\t$(COMPILE_C) $(ZLIBINC)\n\n$(BUILD)/minizip_%.o : 3rd/zlib/contrib/minizip/%.c\n\t$(COMPILE_C) $(ZLIBINC)\n\t\n$(BUILD)/extlua_impl.o : extlua/extlua_impl.c\n\t$(COMPILE_C) $(LUAINC)\n\n$(BUILD)/sokolapi_impl.o : extlua/sokolapi_impl.c\n\t$(COMPILE_C) $(3RDINC)\n\n$(BUILD)/solunaapi_impl.o : extlua/solunaapi_impl.c\n\t$(COMPILE_C) $(LUAINC) $(3RDINC)\n\n$(BIN)/$(APPNAME): $(MAIN_O) $(PLATFORM_O) $(EXTLUA_O) $(LTASK_O) $(LUA_O) $(DATALIST_O) $(BUILD)/yoga.o $(ZLIB_O) $(MINIZIP_O)\n\t$(LD) $(OUTPUT_EXE) $@ $^ $(LDFLAGS)\n\n$(BIN)/sample.dll : extlua/extlua.c extlua/sokolapi.c extlua/solunaapi.c extlua/extlua_sample.c | $(EXTLUA_SHADER_O)\n\t$(CC) $(CFLAGS) $(SHARED) $(OUTPUT_EXE) $@ $^ $(LUAINC) $(3RDINC) $(SHADERINC) -Iextlua\n\nextlua_sample: $(BIN)/sample.dll\n\nclean :\n\trm -f $(BIN)/*.exe $(BIN)/*.dll $(BUILD)/*.o $(BUILD)/*.h\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n\nSokol + Lua = Soluna\n\n</div>\n\n# Soluna\n\n[Live Examples / 在线示例](https://cloudwu.github.io/soluna/)\n\nSoluna is a 2D game framework for Lua. It is built on top of [sokol](https://github.com/floooh/sokol), integrates ltask for multithreading, and runs on Windows, Linux, macOS, and modern browsers through WebAssembly.\n\nSoluna 是一个 Lua 2D 游戏框架。它基于 [sokol](https://github.com/floooh/sokol)，整合 ltask 作为多线程框架，可运行在 Windows、Linux、macOS 以及通过 WebAssembly 支持的现代浏览器中。\n\n[![Nightly](/../../actions/workflows/nightly.yml/badge.svg)](/../../actions/workflows/nightly.yml)\n\n## Documentation / 文档\n\n- [API Reference / API 参考](./docs)\n- [Examples / 示例](./test)\n- [Wiki](https://github.com/cloudwu/soluna/wiki)\n\n## Precompiled Binaries / 预编译二进制文件\n\nPrecompiled binaries for Windows, Linux, macOS, and WebAssembly are available from [Nightly Releases](/../../releases/tag/nightly).\n\nWindows、Linux、macOS 和 WebAssembly 的预编译二进制文件可从 [Nightly Releases](/../../releases/tag/nightly) 下载。\n\n## Building from Source / 从源码构建\n\nSoluna can be built with `make` on Windows and with `luamake` on all supported platforms. The GitHub Action in [`.github/actions/soluna`](./.github/actions/soluna) shows the exact CI build flow.\n\nSoluna 可在 Windows 上通过 `make` 构建，也可在所有支持平台上通过 `luamake` 构建。[`.github/actions/soluna`](./.github/actions/soluna) 展示了 CI 使用的完整构建流程。\n\n### GitHub Actions Integration / GitHub Actions 集成\n\n```yaml\n- uses: actions/checkout@v6\n  with:\n    repository: cloudwu/soluna\n    ref: <a fixed commit hash to avoid breaking changes>\n    path: soluna\n    submodules: recursive\n- uses: ./soluna/.github/actions/soluna\n  id: soluna\n  with:\n    soluna_path: soluna\n- run: |\n    echo \"Soluna binary is at ${{ steps.soluna.outputs.SOLUNA_PATH }}\"\n    echo \"Soluna WASM binary is at ${{ steps.soluna.outputs.SOLUNA_WASM_PATH }}\"\n    echo \"Soluna js glue is at ${{ steps.soluna.outputs.SOLUNA_JS_PATH }}\"\n```\n\n## Local Website Build / 本地构建与运行网站\n\nThe website is an Astro app in `website/`. It renders the homepage from this README, generates API pages from `docs/`, and builds live example pages from `test/`.\n\n网站是位于 `website/` 目录的 Astro 应用。它使用本 README 生成首页，从 `docs/` 生成 API 页面，并从 `test/` 生成在线示例页面。\n\nBuild the WebAssembly runtime from the repository root first:\n\n先在仓库根目录构建 WebAssembly runtime：\n\n```bash\nluamake -compiler emcc\nluamake -compiler emcc sample\n```\n\nThen install dependencies and start the local dev server:\n\n然后安装依赖并启动本地开发服务器：\n\n```bash\ncd website\npnpm install\npnpm run dev\n```\n\n## Projects Made with Soluna / 使用 Soluna 制作的项目\n\n- [Deep Future](https://github.com/cloudwu/deepfuture), a digital version of the board game Deep Future. / 电子版桌游《深远未来》。\n\n## License / 许可证\n\nSoluna is licensed under the MIT License. See [LICENSE](./LICENSE) for details.\n\nSoluna 使用 MIT 许可证。详情见 [LICENSE](./LICENSE)。\n"
  },
  {
    "path": "asset/sounds.dl",
    "content": "--\nname : bloop\nfilename : asset/sounds/bloop_x.wav\n\n--\nname : bloop_loop\nfilename : asset/sounds/bloop_x.wav\ngroup : music\nvolume : 0.6\npitch : 0.8\nloop : true\nstream : true\n"
  },
  {
    "path": "asset/sprites.dl",
    "content": "--\nname : avatar\nfilename : avatar.png\nx : -0.5\ny : -1\n\n--\nname : avatar2\nfilename : avatar.png\nx : -0.5\ny : -0.5\n"
  },
  {
    "path": "clibs/datalist/make.lua",
    "content": "local lm = require \"luamake\"\n\nlm.rootdir = lm.basedir .. \"/3rd/datalist\"\n\nlm:source_set \"datalist_src\" {\n\tsources = {\n\t\t\"datalist.c\",\n\t},\n\tincludes = {\n\t\tlm.basedir .. \"/3rd/lua\",\n\t},\n}\n"
  },
  {
    "path": "clibs/ltask/make.lua",
    "content": "local lm = require \"luamake\"\n\nlm.rootdir = lm.basedir .. \"/3rd/ltask\"\n\nlm:source_set \"ltask_src\" {\n\tsources = {\n\t\t\"src/*.c\",\n\t},\n\tincludes = {\n\t\tlm.basedir .. \"/3rd/lua\",\n\t},\n\tdefines = {\n\t\t\"LTASK_EXTERNAL_OPENLIBS=soluna_openlibs\",\n\t},\n}\n"
  },
  {
    "path": "clibs/lua/make.lua",
    "content": "local lm = require \"luamake\"\n\nlm.rootdir = lm.basedir .. \"/3rd/lua\"\n\nif lm.os == \"windows\" then\n\tlm:source_set \"winfile\" {\n\t\tsources = {\n\t\t\tlm.basedir .. \"/src/winfile.c\",\n\t\t},\n\t}\nend\n\nlm:source_set \"lua_src\" {\n\tsources = {\n\t\t\"onelua.c\",\n\t},\n\tdefines = {\n\t\t\"MAKE_LIB\",\n\t\tlm.os == \"windows\" and \"LUA_DL_DLL\" or \"LUA_USE_DLOPEN\",\n\t},\n}\n\nlm:exe \"lua\" {\n\tdeps = {\n\t\tlm.os == \"windows\" and \"winfile\",\n\t},\n\tsources = {\n\t\t\"onelua.c\",\n\t},\n\tdefines = {\n\t\t\"MAKE_LUA\",\n\t},\n\twindows = {\n\t\tdefines = {\n\t\t\t\"fopen=fopen_utf8\",\n\t\t},\n\t},\n}\n"
  },
  {
    "path": "clibs/sample/make.lua",
    "content": "local lm = require \"luamake\"\nlocal platform = require \"bee.platform\"\n\nlm.rootdir = lm.basedir\n\nlocal function shdc_plat()\n\tif lm.os == \"windows\" then\n\t\treturn \"win32\"\n\tend\n\tif lm.os == \"linux\" then\n\t\treturn \"linux\"\n\tend\n\tif lm.os == \"macos\" then\n\t\treturn platform.Arch == \"arm64\" and \"osx_arm64\" or \"osx\"\n\tend\n\treturn \"unknown\"\nend\n\nlocal paths = {\n\twindows = \"$PATH/$NAME.exe\",\n\tmacos = \"$PATH/$NAME\",\n\tlinux = \"$PATH/$NAME\",\n}\n\nlocal shdc = assert(paths[lm.os]):gsub(\"%$(%u+)\", {\n\tPATH = tostring(lm.basedir / \"bin/sokol-tools-bin/bin\" / shdc_plat()),\n\tNAME = \"sokol-shdc\",\n})\n\nlocal function shader_lang()\n\tlocal plat = lm.platform\n\tif plat == \"msvc\" or plat == \"clang-cl\" or plat == \"mingw\" then\n\t\treturn \"hlsl4\"\n\tend\n\tif plat == \"macos\" then\n\t\treturn \"metal_macos\"\n\tend\n\tif plat == \"emcc\" then\n\t\treturn \"wgsl\"\n\tend\n\tif plat == \"linux\" then\n\t\treturn \"glsl430\"\n\tend\n\treturn \"unknown\"\nend\n\nlocal function compile_shader(src, name)\n\tlocal dep = name .. \"_shader\"\n\tlocal target = lm.builddir .. \"/\" .. name\n\tlm:runlua(dep) {\n\t\tscript = lm.basedir .. \"/clibs/soluna/shader2c.lua\",\n\t\tinputs = lm.basedir .. \"/\" .. src,\n\t\toutputs = lm.basedir .. \"/\" .. target,\n\t\targs = {\n\t\t\tshdc,\n\t\t\t\"$in\",\n\t\t\t\"$out\",\n\t\t\tshader_lang(),\n\t\t},\n\t}\n\treturn dep\nend\n\nlocal sample_shader = compile_shader(\"extlua/perspective_quad.glsl\", \"perspective_quad.glsl.h\")\n\nlm:dll \"sample\" {\n\tsources = {\n\t\t\"extlua/extlua.c\",\n\t\t\"extlua/sokolapi.c\",\n\t\t\"extlua/solunaapi.c\",\n\t\t\"extlua/extlua_sample.c\",\n\t},\n\tobjdeps = {\n\t\tsample_shader,\n\t},\n\tincludes = {\n\t\t\"3rd/lua\",\n\t\t\"3rd\",\n\t\t\"build\",\n\t\t\"extlua\",\n\t},\n}\n"
  },
  {
    "path": "clibs/soluna/compile_lua.lua",
    "content": "local lm = require \"luamake\"\nlocal fs = require \"bee.filesystem\"\n\nlocal function compile_lua_code(script, src, name)\n\tlocal dep = name .. \"_lua_code\"\n\tlocal target = lm.builddir .. \"/\" .. name\n\tlocal bindir = lm.bindir\n\tif lm.platform == \"emcc\" then\n\t\tbindir = lm.osbindir\n\tend\n\tlm:runlua(dep) {\n\t\tscript = lm.basedir .. \"/clibs/soluna/runlua.lua\",\n\t\tdeps = {\n\t\t\t\"lua\",\n\t\t},\n\t\tinputs = lm.basedir .. \"/\" .. src,\n\t\toutputs = lm.basedir .. \"/\" .. target,\n\t\targs = {\n\t\t\tbindir,\n\t\t\tlm.basedir .. \"/\" .. script,\n\t\t\t\"$in\",\n\t\t\t\"$out\",\n\t\t},\n\t}\n\treturn dep\nend\n\nlocal lua_code_src = {\n\t\"3rd/ltask/service\",\n\t\"3rd/ltask/lualib\",\n\t\"src/service\",\n\t\"src/lualib\",\n\t\"src/material\",\n}\n\nreturn function(objdeps)\n\tfor _, dir in ipairs(lua_code_src) do\n\t\tfor path in fs.pairs(lm.basedir .. \"/\" .. dir) do\n\t\t\tif path:extension() == \".lua\" then\n\t\t\t\tlocal base = path:stem():string()\n\t\t\t\tlocal dep = compile_lua_code(\"script/lua2c.lua\", path:string(), base .. \".lua.h\")\n\t\t\t\tobjdeps[#objdeps + 1] = dep\n\t\t\tend\n\t\tend\n\tend\n\n\tfor path in fs.pairs \"src/data\" do\n\t\tif path:extension() == \".dl\" then\n\t\t\tlocal base = path:stem():string()\n\t\t\tlocal dep = compile_lua_code(\"script/datalist2c.lua\", path:string(), base .. \".dl.h\")\n\t\t\tobjdeps[#objdeps + 1] = dep\n\t\tend\n\tend\nend\n"
  },
  {
    "path": "clibs/soluna/compile_shader.lua",
    "content": "local lm = require \"luamake\"\nlocal fs = require \"bee.filesystem\"\nlocal platform = require \"bee.platform\"\n\nlocal function shdc_plat()\n\tif lm.os == \"windows\" then\n\t\treturn \"win32\"\n\tend\n\tif lm.os == \"linux\" then\n\t\treturn \"linux\"\n\tend\n\tif lm.os == \"macos\" then\n\t\treturn platform.Arch == \"arm64\" and \"osx_arm64\" or \"osx\"\n\tend\n\treturn \"unknown\"\nend\nlocal paths = {\n\twindows = \"$PATH/$NAME.exe\",\n\tmacos = \"$PATH/$NAME\",\n\tlinux = \"$PATH/$NAME\",\n}\nlocal shdc = assert(paths[lm.os]):gsub(\"%$(%u+)\", {\n\tPATH = tostring(lm.basedir / \"bin/sokol-tools-bin/bin\" / shdc_plat()),\n\tNAME = \"sokol-shdc\",\n})\n\nlocal function compile_shader(src, name, lang)\n\tlocal dep = name .. \"_shader\"\n\tlocal target = lm.builddir .. \"/\" .. name\n\tlm:runlua(dep) {\n\t\tscript = lm.basedir .. \"/clibs/soluna/shader2c.lua\",\n\t\tinputs = lm.basedir .. \"/\" .. src,\n\t\toutputs = lm.basedir .. \"/\" .. target,\n\t\targs = {\n\t\t\tshdc,\n\t\t\t\"$in\",\n\t\t\t\"$out\",\n\t\t\tlang,\n\t\t},\n\t}\n\treturn dep\nend\n\nlocal function shader_lang()\n\tlocal plat = lm.platform\n\tif plat == \"msvc\" or plat == \"clang-cl\" or plat == \"mingw\" then\n\t\treturn \"hlsl4\"\n\tend\n\tif plat == \"macos\" then\n\t\treturn \"metal_macos\"\n\tend\n\tif plat == \"emcc\" then\n\t\treturn \"wgsl\"\n\tend\n\tif plat == \"linux\" then\n\t\treturn \"glsl430\"\n\tend\n\treturn \"unknown\"\nend\n\nreturn function(objdeps)\n\tfor path in fs.pairs \"src\" do\n\t\tlocal lang = shader_lang()\n\t\tif path:extension() == \".glsl\" then\n\t\t\tlocal base = path:stem():string()\n\t\t\tlocal dep = compile_shader(path:string(), base .. \".glsl.h\", lang)\n\t\t\tobjdeps[#objdeps + 1] = dep\n\t\tend\n\tend\nend\n"
  },
  {
    "path": "clibs/soluna/make.lua",
    "content": "local lm = require \"luamake\"\nlocal subprocess = require \"bee.subprocess\"\nlocal compile_lua = require \"compile_lua\"\nlocal compile_shader = require \"compile_shader\"\n\nlm.rootdir = lm.basedir\n\nlocal ok, process, errMsg = pcall(subprocess.spawn, {\n\tlm.os ~= \"windows\" and \"git\" or \"C:\\\\Program Files\\\\Git\\\\cmd\\\\git.exe\",\n\t\"rev-parse\",\n\t\"HEAD\",\n\tstdout = true,\n})\nlocal commit\nif ok then\n\tif errMsg then\n\t\tprint(\"Failed to start git process: \" .. errMsg)\n\telse\n\t\tlocal output = process.stdout:read \"a\"\n\t\tcommit = output:match \"^%s*(.-)%s*$\"\n\t\tprocess:wait()\n\tend\nend\n\nlocal objdeps = {}\n\ncompile_lua(objdeps)\ncompile_shader(objdeps)\n\nlm:source_set \"soluna_src\" {\n\tsources = {\n\t\t\"src/*.c\",\n\t\t\"extlua/extlua_impl.c\",\n\t\t\"extlua/sokolapi_impl.c\",\n\t\t\"extlua/solunaapi_impl.c\",\n\t},\n\tobjdeps = objdeps,\n\tdefines = {\n\t\tcommit and string.format('SOLUNA_HASH_VERSION=\\\\\"%s\\\\\"', commit),\n\t},\n\tincludes = {\n\t\t\"build\",\n\t\t\"src\",\n\t\t\"3rd\",\n\t\t\"3rd/lua\",\n\t\t\"3rd/yoga\",\n\t\t\"3rd/zlib\",\n\t\t\"3rd/miniaudio\",\n\t},\n\tclang = {\n\t\tsources = lm.os == \"macos\" and {\n\t\t\t\"src/platform/macos/*.m\",\n\t\t},\n\t\tflags = lm.os == \"macos\" and {\n\t\t\t\"-x objective-c\",\n\t\t},\n\t\tframeworks = lm.os == \"macos\" and {\n\t\t\t\"AudioToolbox\",\n\t\t\t\"IOKit\",\n\t\t\t\"CoreText\",\n\t\t\t\"CoreFoundation\",\n\t\t\t\"Foundation\",\n\t\t\t\"Cocoa\",\n\t\t\t\"Metal\",\n\t\t\t\"MetalKit\",\n\t\t\t\"QuartzCore\",\n\t\t},\n\t},\n\twindows = {\n\t\tsources = {\n\t\t\t\"src/platform/windows/*.c\",\n\t\t},\n\t\tincludes = {\n\t\t\t\"3rd/zlib/contrib/minizip\",\n\t\t}\n\t},\n\tgcc = {\n\t\tsources = lm.os == \"linux\" and {\n\t\t\t\"src/platform/linux/*.c\",\n\t\t} or nil,\n\t\tlinks = lm.os == \"linux\" and {\n\t\t\t\"pthread\",\n\t\t\t\"dl\",\n\t\t\t\"GL\",\n\t\t\t\"X11\",\n\t\t\t\"Xrandr\",\n\t\t\t\"Xi\",\n\t\t\t\"Xxf86vm\",\n\t\t\t\"Xcursor\",\n\t\t\t\"GLU\",\n\t\t\t\"asound\",\n\t\t},\n\t},\n\tmsvc = {\n\t\tldflags = {\n\t\t\t\"-SUBSYSTEM:WINDOWS\",\n\t\t\t\"xinput.lib\",\n\t\t\t\"Ws2_32.lib\",\n\t\t\t\"ntdll.lib\",\n\t\t\t\"Imm32.lib\",\n\t\t},\n\t},\n\tmingw = {\n\t\tlinks = {\n\t\t\t\"kernel32\",\n\t\t\t\"user32\",\n\t\t\t\"shell32\",\n\t\t\t\"gdi32\",\n\t\t\t\"dxgi\",\n\t\t\t\"d3d11\",\n\t\t\t\"winmm\",\n\t\t\t\"ws2_32\",\n\t\t\t\"ntdll\",\n\t\t\t\"xinput\",\n\t\t\t\"imm32\",\n\t\t},\n\t\tflags = {\n\t\t\t\"-Wl,subsystem,windows\",\n\t\t},\n\t},\n}\n"
  },
  {
    "path": "clibs/soluna/runlua.lua",
    "content": "local subprocess = require \"bee.subprocess\"\nlocal platform = require \"bee.platform\"\n\nlocal bindir, script, src, target = ...\n\nlocal luaexe = platform.os == \"windows\" and bindir .. \"/lua.exe\" or bindir .. \"/lua\"\n\nlocal process = assert(subprocess.spawn {\n\tluaexe, script, src, target,\n})\n\nlocal code = process:wait()\nif code ~= 0 then\n\tos.exit(code, true)\nend\n"
  },
  {
    "path": "clibs/soluna/shader2c.lua",
    "content": "local subprocess = require \"bee.subprocess\"\nlocal shdcexe, src, target, lang = ...\n\nlocal process = assert(subprocess.spawn {\n\tshdcexe,\n\t\"--input\",\n\tsrc,\n\t\"--output\",\n\ttarget,\n\t\"--slang\",\n\tlang,\n\t\"--format\",\n\t\"sokol\",\n})\n\nlocal code = process:wait()\nif code ~= 0 then\n\tos.exit(code, true)\nend\n"
  },
  {
    "path": "clibs/yoga/make.lua",
    "content": "local lm = require \"luamake\"\n\nlm.rootdir = lm.basedir .. \"/3rd/yoga\"\n\nlm:source_set \"yoga_src\" {\n\tsources = {\n\t\t\"yoga/*.cpp\",\n\t\t\"yoga/*/*.cpp\",\n\t},\n\tincludes = {\n\t\tlm.rootdir,\n\t}\n}\n"
  },
  {
    "path": "clibs/zip/make.lua",
    "content": "local lm = require \"luamake\"\n\nlm.rootdir = lm.basedir .. \"/3rd/zlib\"\n\nlm:source_set \"minizip\" {\n\tsources = {\n\t\t\"contrib/minizip/ioapi.c\",\n\t\t\"contrib/minizip/unzip.c\",\n\t\t\"contrib/minizip/zip.c\",\n\t},\n\twindows = {\n\t\tsources = {\n\t\t\t\"contrib/minizip/iowin32.c\",\n\t\t},\n\t\tincludes = {\n\t\t\t\"contrib/minizip\",\n\t\t},\n\t},\n\tincludes = {\n\t\tlm.rootdir,\n\t},\n\n}\n\nlm:source_set \"zlib\" {\n\tsources = {\n\t\t\"*.c\",\n\t\t\"!gz*.c\",\n\t},\n}\n\nlm:source_set \"zip_src\" {\n\tdeps = {\n\t\t\"minizip\",\n\t\t\"zlib\",\n\t}\n}\n"
  },
  {
    "path": "docs/app.lua",
    "content": "---@meta soluna.app\n\n---输入法候选窗口矩形\n---IME candidate window rectangle.\n---@class soluna.app.ImeRect\n---@field x number 左上角 X / Left coordinate\n---@field y number 左上角 Y / Top coordinate\n---@field width number 宽度 / Width\n---@field height number 高度 / Height\n---@field text_color? integer 文本 ARGB 颜色；alpha 为 0 时补为 0xff / Text color in ARGB; alpha 0 is promoted to 0xff\n\n---应用控制模块\n---Application control module.\n---@class soluna.app\nlocal app = {}\n\n---请求应用优雅退出\n---Requests graceful application quit.\nfunction app.quit()\nend\n\n---设置输入法字体\n---Sets the IME font face and pixel size.\n---@overload fun()\n---@overload fun(font_size: number)\n---@param font_name? string 字体名；nil 表示平台默认字体 / Font face; nil uses platform default\n---@param font_size number 字体像素大小 / Font size in pixels\nfunction app.set_ime_font(font_name, font_size)\nend\n\n---设置输入法候选窗口矩形\n---Sets the IME candidate window rectangle.\n---@param rect? soluna.app.ImeRect nil 会清除矩形 / nil clears the rectangle\nfunction app.set_ime_rect(rect)\nend\n\nreturn app\n"
  },
  {
    "path": "docs/args.lua",
    "content": "---@meta\n\n---可提交给 batch 的绘制对象\n---Drawable object accepted by `Batch:add`.\n---@alias soluna.Drawable integer|string|userdata\n\n---绘制批次\n---Render batch object.\n---@class Batch\nlocal batch = {}\n\n---向批次添加 sprite、material 对象或 packed stream\n---Adds a sprite id, material userdata, or packed command stream.\n---@param sprite soluna.Drawable sprite ID、material userdata 或 packed string / Sprite id, material userdata, or packed string\n---@param x? number X 坐标，默认 0 / X position, default 0\n---@param y? number Y 坐标，默认 0 / Y position, default 0\nfunction batch:add(sprite, x, y)\nend\n\n---打开或关闭变换层\n---Opens or closes a transform layer.\n---@overload fun(self: Batch)\n---@overload fun(self: Batch, rotation: number)\n---@overload fun(self: Batch, x: number, y: number)\n---@overload fun(self: Batch, scale: number, x: number, y: number)\n---@param scale number 缩放倍率，不能为 0 / Scale factor, cannot be 0\n---@param rotation number 旋转弧度 / Rotation in radians\n---@param x number X 平移 / X translation\n---@param y number Y 平移 / Y translation\nfunction batch:layer(scale, rotation, x, y)\nend\n\n---把屏幕点转换到当前 layer 坐标\n---Transforms a screen point into the current layer space.\n---@param x number 屏幕 X / Screen X\n---@param y number 屏幕 Y / Screen Y\n---@return number x 转换后的 X / Transformed X\n---@return number y 转换后的 Y / Transformed Y\nfunction batch:point(x, y)\nend\n\n---入口参数表\n---Entry argument table passed to the game script.\n---@class Args\n---@field width integer 当前窗口宽度 / Current window width\n---@field height integer 当前窗口高度 / Current window height\n---@field batch Batch 绘制批次 / Render batch\n---@field [integer] string 启动参数 / Startup argument\nlocal args = {}\n\nreturn args\n"
  },
  {
    "path": "docs/callback.lua",
    "content": "---@meta\n\n---游戏入口返回的 callback 表\n---Callback table returned by the game entry script.\n---@class Callback\nlocal callback = {}\n\n---每帧调用\n---Called once per frame.\n---@param count integer frame 计数 / Frame counter\nfunction callback.frame(count)\nend\n\n---键盘事件\n---Keyboard event.\n---@param keycode integer Sokol key code / Sokol key code\n---@param state integer 1 为按下，0 为释放 / 1 for key down, 0 for key up\nfunction callback.key(keycode, state)\nend\n\n---字符输入事件\n---Text input event.\n---@param codepoint integer Unicode codepoint / Unicode codepoint\nfunction callback.char(codepoint)\nend\n\n---鼠标按钮事件\n---Mouse button event.\n---@param button integer 0 左键，1 右键，2 中键 / 0 left, 1 right, 2 middle\n---@param state integer 1 为按下，0 为释放 / 1 for down, 0 for up\nfunction callback.mouse_button(button, state)\nend\n\n---鼠标移动事件\n---Mouse move event.\n---@param x integer 逻辑像素 X / Logical pixel X\n---@param y integer 逻辑像素 Y / Logical pixel Y\nfunction callback.mouse_move(x, y)\nend\n\n---鼠标滚轮事件\n---Mouse scroll event.\n---@param y integer 垂直滚动量 / Vertical scroll delta\n---@param x integer 水平滚动量 / Horizontal scroll delta\nfunction callback.mouse_scroll(y, x)\nend\n\n---其它鼠标事件\n---Other mouse event.\n---@param event_type integer Sokol event type / Sokol event type\nfunction callback.mouse(event_type)\nend\n\n---触摸开始\n---Touch begin event.\n---@param x integer 逻辑像素 X / Logical pixel X\n---@param y integer 逻辑像素 Y / Logical pixel Y\nfunction callback.touch_begin(x, y)\nend\n\n---触摸移动\n---Touch move event.\n---@param x integer 逻辑像素 X / Logical pixel X\n---@param y integer 逻辑像素 Y / Logical pixel Y\nfunction callback.touch_moved(x, y)\nend\n\n---触摸结束\n---Touch end event.\n---@param x integer 逻辑像素 X / Logical pixel X\n---@param y integer 逻辑像素 Y / Logical pixel Y\nfunction callback.touch_end(x, y)\nend\n\n---触摸取消\n---Touch cancelled event.\n---@param x integer 逻辑像素 X / Logical pixel X\n---@param y integer 逻辑像素 Y / Logical pixel Y\nfunction callback.touch_cancelled(x, y)\nend\n\n---窗口尺寸变化\n---Window resize event.\n---@param width integer 新窗口宽度 / New window width\n---@param height integer 新窗口高度 / New window height\nfunction callback.window_resize(width, height)\nend\n\nreturn callback\n"
  },
  {
    "path": "docs/coroutine.lua",
    "content": "---@meta soluna.coroutine\n\n---ltask 兼容 coroutine 模块\n---ltask-compatible coroutine module.\n---@class soluna.coroutine\nlocal coroutine = {}\n\n---创建受 ltask 跟踪的 coroutine\n---Creates a coroutine tracked by the ltask bridge.\n---@param f function coroutine 函数 / Coroutine function\n---@return thread co coroutine 线程 / Coroutine thread\nfunction coroutine.create(f)\nend\n\n---恢复 coroutine\n---Resumes a tracked coroutine.\n---@param co thread coroutine 线程 / Coroutine thread\n---@param ... any 传入参数 / Arguments\n---@return boolean ok 是否成功 / Whether resume succeeded\n---@return any ... 返回值或错误 / Return values or error\nfunction coroutine.resume(co, ...)\nend\n\n---挂起当前 coroutine\n---Yields from the current coroutine.\n---@param ... any 返回给 resume 的值 / Values returned to resume\n---@return any ... 下次 resume 传入的值 / Values passed by the next resume\nfunction coroutine.yield(...)\nend\n\nreturn coroutine\n"
  },
  {
    "path": "docs/crypt.lua",
    "content": "---@meta soluna.crypt\n\n---密码辅助模块\n---Cryptography helper module.\n---@class soluna.crypt\nlocal crypt = {}\n\n---编码为小写十六进制字符串\n---Encodes binary data as lower-case hex.\n---@param data string 二进制数据 / Binary data\n---@return string hex 十六进制字符串 / Hex string\nfunction crypt.hexencode(data)\nend\n\n---计算 SHA-1 摘要\n---Calculates SHA-1 digest.\n---@param data string 输入数据 / Input data\n---@return string hash 20 字节摘要 / 20-byte digest\nfunction crypt.sha1(data)\nend\n\nreturn crypt\n"
  },
  {
    "path": "docs/datalist.lua",
    "content": "---@meta soluna.datalist\n\n---datalist 解析模块\n---Datalist parser module.\n---@class soluna.datalist\nlocal datalist = {}\n\n---解析 datalist 文本\n---Parses datalist text.\n---@param data string datalist 文本 / Datalist text\n---@return table parsed 解析结果 / Parsed result\nfunction datalist.parse(data)\nend\n\n---为 datalist 格式引用字符串\n---Quotes a string for datalist syntax.\n---@param str string 原始字符串 / Raw string\n---@return string quoted quoted 字符串 / Quoted string\nfunction datalist.quote(str)\nend\n\nreturn datalist\n"
  },
  {
    "path": "docs/file.lua",
    "content": "---@meta soluna.file\n\n---文件属性表\n---File attribute table.\n---@class soluna.file.Attributes\n---@field mode \"file\"|\"directory\"|\"link\"|\"socket\"|\"named pipe\"|\"char device\"|\"block device\"|\"other\" 文件类型 / File type\n---@field dev integer 设备号 / Device id\n---@field ino integer inode / Inode\n---@field nlink integer 硬链接数 / Hard link count\n---@field uid integer owner user id / Owner user id\n---@field gid integer owner group id / Owner group id\n---@field rdev integer special file device id / Special file device id\n---@field access integer 最后访问时间 / Last access time\n---@field modification integer 最后修改时间 / Last modification time\n---@field change integer 最后状态变化时间 / Last status change time\n---@field size integer 文件大小 / File size\n---@field permissions string 权限字符串 / Permission string\n\n---文件加载模块\n---File loading module.\n---@class soluna.file\nlocal file = {}\n\n---加载文件内容\n---Loads file contents.\n---@param filename string 文件路径 / File path\n---@param mode? string 本地文件打开模式，默认 `\"rb\"` / Local file open mode, default `\"rb\"`\n---@return string? content 文件内容；失败返回 nil / File contents, nil on failure\nfunction file.load(filename, mode)\nend\n\n---获取文件属性\n---Gets file attributes.\n---@param filename string 文件路径 / File path\n---@return soluna.file.Attributes|string? attributes 本地文件返回属性表，zip 文件可返回 `\"file\"` 或 `\"directory\"` / Local files return attributes; zip files may return `\"file\"` or `\"directory\"`\nfunction file.attributes(filename)\nend\n\n---判断文件是否存在\n---Checks whether a file exists.\n---@param filename string 文件路径 / File path\n---@return boolean? exists 存在时为 true，否则为 nil / true when found, nil otherwise\nfunction file.exist(filename)\nend\n\n---判断本地文件是否存在\n---Checks whether a local file exists.\n---@param filename string 文件路径 / File path\n---@return boolean? exists 存在时为 true，否则为 nil / true when found, nil otherwise\nfunction file.local_exist(filename)\nend\n\n---加载本地文件内容\n---Loads local file contents.\n---@param filename string 文件路径 / File path\n---@param mode? string 打开模式，默认 `\"rb\"` / Open mode, default `\"rb\"`\n---@return string? content 文件内容；失败返回 nil / File contents, nil on failure\nfunction file.local_load(filename, mode)\nend\n\n---遍历目录条目\n---Iterates directory entries.\n---@param path string 目录路径 / Directory path\n---@return fun(): string? iterator 迭代器 / Iterator\n---@return userdata? state 本地目录句柄 / Local directory handle\nfunction file.dir(path)\nend\n\nreturn file\n"
  },
  {
    "path": "docs/font.lua",
    "content": "---@meta soluna.font\n\n---字体模块\n---Font module.\n---@class soluna.font\nlocal font = {}\n\n---导入 TrueType 字体数据\n---Imports TrueType font data.\n---@param data string TTF/TTC 字体数据 / TTF/TTC font data\nfunction font.import(data)\nend\n\n---按字体族名获取 font id\n---Gets a font id by family name.\n---@param name string 字体族名 / Font family name\n---@return integer? fontid 字体 id；找不到时为 nil / Font id, nil when not found\nfunction font.name(name)\nend\n\n---返回字体管理器 C 指针\n---Returns the native font manager pointer.\n---@return lightuserdata fontcobj 字体管理器指针 / Font manager pointer\nfunction font.cobj()\nend\n\nreturn font\n"
  },
  {
    "path": "docs/font_system.lua",
    "content": "---@meta soluna.font.system\n\n---系统字体模块\n---System font module.\n---@class soluna.font.system\nlocal font_system = {}\n\n---按字体族名读取系统 TTF/TTC 数据\n---Reads system TTF/TTC data by family name.\n---@param name string 字体族名 / Font family name\n---@return string? data 字体数据；wasm 或失败时可能为 nil / Font data, nil on wasm or failure\nfunction font_system.ttfdata(name)\nend\n\nreturn font_system\n"
  },
  {
    "path": "docs/image.lua",
    "content": "---@meta soluna.image\n\n---图片模块\n---Image module.\n---@class soluna.image\nlocal image = {}\n\n---从 PNG 数据加载 RGBA 图片\n---Loads RGBA image data from PNG bytes.\n---@param data string PNG 数据 / PNG bytes\n---@return string? data RGBA 像素数据，失败时为 nil / RGBA pixels, nil on failure\n---@return integer|string width_or_error 成功时为宽度，失败时为错误信息 / Width on success, error message on failure\n---@return integer? height 高度 / Height\nfunction image.load(data)\nend\n\n---按比例缩放 RGBA 或灰度图片\n---Resizes RGBA or grayscale image data by scale factors.\n---@param data string RGBA 或灰度像素数据 / RGBA or grayscale pixels\n---@param width integer 原始宽度 / Source width\n---@param height integer 原始高度 / Source height\n---@param scale_x number X 缩放倍率 / X scale factor\n---@param scale_y? number Y 缩放倍率，默认等于 `scale_x` / Y scale factor, default is `scale_x`\n---@return string data 缩放后的像素数据 / Resized pixels\n---@return integer width 新宽度 / New width\n---@return integer height 新高度 / New height\nfunction image.resize(data, width, height, scale_x, scale_y)\nend\n\nreturn image\n"
  },
  {
    "path": "docs/layout.lua",
    "content": "---@meta soluna.layout\n\n---layout 元素对象\n---Layout element object.\n---@class soluna.layout.Element\nlocal element = {}\n\n---更新元素 Yoga 属性\n---Updates Yoga attributes on the element.\n---@param attr table 属性表 / Attribute table\nfunction element:update(attr)\nend\n\n---读取元素布局结果\n---Reads calculated element layout.\n---@return number x X 坐标 / X coordinate\n---@return number y Y 坐标 / Y coordinate\n---@return number w 宽度 / Width\n---@return number h 高度 / Height\nfunction element:get()\nend\n\n---返回元素属性表\n---Returns the element attribute table.\n---@return table attrs 属性表 / Attribute table\nfunction element:attribs()\nend\n\n---layout 文档对象\n---Layout document object.\n---@class soluna.layout.Document\n---@field [string] soluna.layout.Element 按 id 访问元素 / Element access by id\n\n---layout 绘制条目\n---Calculated drawable layout item.\n---@class soluna.layout.Item\n---@field x number X 坐标 / X coordinate\n---@field y number Y 坐标 / Y coordinate\n---@field w number 宽度 / Width\n---@field h number 高度 / Height\n---@field [string] any datalist 属性 / Datalist attributes\n\n---layout 计算结果\n---Calculated layout item list.\n---@class soluna.layout.Result\n---@field [integer] soluna.layout.Item 绘制条目 / Drawable item\n---@field width number 根节点宽度 / Root width\n---@field height number 根节点高度 / Root height\n\n---Yoga layout 模块\n---Yoga layout module.\n---@class soluna.layout\nlocal layout = {}\n\n---加载 layout 定义\n---Loads a layout definition.\n---@param filename_or_list string|table layout 文件路径或已解析 datalist / Layout file path or parsed datalist\n---@param scripts? fun(name: string): table children 动态 children resolver / Dynamic children resolver\n---@return soluna.layout.Document document layout 文档 / Layout document\nfunction layout.load(filename_or_list, scripts)\nend\n\n---计算 layout 并返回绘制条目\n---Calculates layout and returns drawable items.\n---@param document soluna.layout.Document layout 文档 / Layout document\n---@return soluna.layout.Result items 绘制条目列表 / Drawable item list\nfunction layout.calc(document)\nend\n\nreturn layout\n"
  },
  {
    "path": "docs/lfs.lua",
    "content": "---@meta soluna.lfs\n\n---本地文件属性表\n---Local file attribute table.\n---@class soluna.lfs.Attributes\n---@field mode \"file\"|\"directory\"|\"link\"|\"socket\"|\"named pipe\"|\"char device\"|\"block device\"|\"other\" 文件类型 / File type\n---@field size integer 文件大小 / File size\n---@field access integer 最后访问时间 / Last access time\n---@field modification integer 最后修改时间 / Last modification time\n---@field change integer 最后状态变化时间 / Last status change time\n---@field permissions string 权限字符串 / Permission string\n\n---本地文件系统模块\n---Local filesystem module.\n---@class soluna.lfs\nlocal lfs = {}\n\n---获取文件属性\n---Gets file attributes.\n---@param filename string 文件路径 / File path\n---@param member? string 可选属性名 / Optional attribute name\n---@return soluna.lfs.Attributes|integer|string|nil attributes 属性表或指定属性 / Attribute table or selected attribute\n---@return string? err 错误信息 / Error message\n---@return integer? errno 系统错误码 / System errno\nfunction lfs.attributes(filename, member)\nend\n\n---遍历目录条目\n---Iterates directory entries.\n---@param path string 目录路径 / Directory path\n---@return fun(): string? iterator 迭代器 / Iterator\n---@return userdata state 目录句柄 / Directory handle\nfunction lfs.dir(path)\nend\n\nreturn lfs\n"
  },
  {
    "path": "docs/material_mask.lua",
    "content": "---@meta soluna.material.mask\n\n---mask material 模块\n---Mask material module.\n---@class soluna.material.mask\nlocal matmask = {}\n\n---创建带颜色遮罩的 sprite command stream\n---Creates a colored mask sprite command stream.\n---@param sprite integer 1-based sprite id / 1-based sprite id\n---@param color integer ARGB 颜色，alpha 为 0 时补为 0xff / ARGB color; alpha 0 is promoted to 0xff\n---@return string stream 可传给 `batch:add` 的 packed stream / Packed stream for `batch:add`\nfunction matmask.mask(sprite, color)\nend\n\nreturn matmask\n"
  },
  {
    "path": "docs/material_quad.lua",
    "content": "---@meta soluna.material.quad\n\n---quad material 模块\n---Quad material module.\n---@class soluna.material.quad\nlocal matquad = {}\n\n---创建纯色矩形 command stream\n---Creates a solid rectangle command stream.\n---@param width integer 宽度 / Width\n---@param height integer 高度 / Height\n---@param color integer ARGB 颜色，alpha 为 0 时补为 0xff / ARGB color; alpha 0 is promoted to 0xff\n---@return string stream 可传给 `batch:add` 的 packed stream / Packed stream for `batch:add`\nfunction matquad.quad(width, height, color)\nend\n\nreturn matquad\n"
  },
  {
    "path": "docs/material_text.lua",
    "content": "---@meta soluna.material.text\n\n---文本块创建函数\n---Text block builder function.\n---@alias soluna.material.text.Block fun(text: string, width?: integer, height?: integer): string, integer\n\n---光标位置查询函数\n---Text cursor query function.\n---@alias soluna.material.text.Cursor fun(text: string, position: integer, width?: integer, height?: integer): integer, integer, integer, integer, integer, integer\n\n---text material 模块\n---Text material module.\n---@class soluna.material.text\nlocal mattext = {}\n\n---创建文本块和光标查询函数\n---Creates text block and cursor query functions.\n---@param fontcobj lightuserdata `font.cobj()` 返回的字体管理器指针 / Font manager pointer returned by `font.cobj()`\n---@param fontid integer `font.name()` 返回的字体 id / Font id returned by `font.name()`\n---@param size? integer 字体像素大小，默认 16 / Font pixel size, default 16\n---@param color? integer ARGB 颜色，默认 `0xff000000` / ARGB color, default `0xff000000`\n---@param alignment? string 对齐代码，如 `\"LT\"`、`\"CV\"`、`\"RB\"` / Alignment code such as `\"LT\"`, `\"CV\"`, `\"RB\"`\n---@return soluna.material.text.Block block 创建 packed text stream / Creates packed text stream\n---@return soluna.material.text.Cursor cursor 查询光标矩形 / Queries cursor rectangle\nfunction mattext.block(fontcobj, fontid, size, color, alignment)\nend\n\nreturn mattext\n"
  },
  {
    "path": "docs/soluna.lua",
    "content": "---@meta\n\n---单个 sprite id 或动画帧 id 列表\n---Single sprite id or animation frame id list.\n---@alias Sprite integer|integer[]\n\n---sprite bundle 名称到 id 的映射\n---Sprite bundle name-to-id mapping.\n---@alias SpriteBundle table<string, Sprite?>\n\n---窗口图标图片描述\n---Window icon image descriptor.\n---@class soluna.IconImage\n---@field data string|userdata|lightuserdata RGBA 像素数据 / RGBA pixel buffer\n---@field w? integer 宽度；也可使用 `width` / Width; `width` is also accepted\n---@field h? integer 高度；也可使用 `height` / Height; `height` is also accepted\n---@field width? integer 宽度；`w` 的别名 / Width alias for `w`\n---@field height? integer 高度；`h` 的别名 / Height alias for `h`\n---@field stride? integer 每行字节数，默认 `width * 4` / Row stride in bytes, default `width * 4`\n---@field size? integer `lightuserdata` 数据大小 / Buffer size for `lightuserdata`\n\n---运行时预加载 sprite 图片\n---Runtime preloaded sprite image.\n---@class soluna.PreloadSprite\n---@field filename string 虚拟文件名 / Virtual filename\n---@field content string RGBA 像素数据 / RGBA pixel data\n---@field w integer 宽度 / Width\n---@field h integer 高度 / Height\n\n---音频播放选项\n---Audio playback options.\n---@class soluna.AudioPlayOptions\n---@field group? string audio bus 名称 / Audio bus name\n---@field volume? number 线性音量倍率 / Linear volume multiplier\n---@field pan? number 声像，范围通常为 `[-1.0, 1.0]` / Stereo pan, usually in `[-1.0, 1.0]`\n---@field pitch? number pitch 倍率 / Pitch multiplier\n---@field loop? boolean 是否循环播放 / Whether playback loops\n---@field stream? boolean 是否流式播放 / Whether to stream instead of preloading\n\n---音频播放实例\n---Audio playback instance.\n---@class soluna.AudioVoice\nlocal AudioVoice = {}\n\n---停止播放\n---Stops playback.\n---@param fade_seconds? number fade out 秒数 / Fade-out seconds\n---@return boolean ok voice 有效且请求成功时为 true / true when the voice is valid and the request succeeds\nfunction AudioVoice:stop(fade_seconds)\nend\n\n---返回是否仍在播放\n---Returns whether the voice is still playing.\n---@return boolean playing 是否播放中 / Whether it is playing\nfunction AudioVoice:playing()\nend\n\n---设置 voice 音量\n---Sets voice volume.\n---@param volume number 线性音量倍率 / Linear volume multiplier\n---@return boolean ok voice 有效时为 true / true when the voice is valid\nfunction AudioVoice:set_volume(volume)\nend\n\n---设置 voice 声像\n---Sets voice pan.\n---@param pan number 声像 / Stereo pan\n---@return boolean ok voice 有效时为 true / true when the voice is valid\nfunction AudioVoice:set_pan(pan)\nend\n\n---设置 voice pitch\n---Sets voice pitch.\n---@param pitch number pitch 倍率 / Pitch multiplier\n---@return boolean ok voice 有效时为 true / true when the voice is valid\nfunction AudioVoice:set_pitch(pitch)\nend\n\n---设置 voice 是否循环\n---Sets whether the voice loops.\n---@param loop boolean 是否循环 / Whether to loop\n---@return boolean ok voice 有效时为 true / true when the voice is valid\nfunction AudioVoice:set_loop(loop)\nend\n\n---跳转播放位置\n---Seeks to a playback position.\n---@param seconds number 目标秒数 / Target seconds\n---@return boolean ok voice 有效且 seek 成功时为 true / true when valid and seek succeeds\nfunction AudioVoice:seek(seconds)\nend\n\n---返回当前播放位置\n---Returns the current playback position.\n---@return number? seconds 当前秒数 / Current seconds\n---@return string? err 错误信息 / Error message\nfunction AudioVoice:tell()\nend\n\n---音频 bus 句柄\n---Audio bus handle.\n---@class soluna.AudioBus\nlocal AudioBus = {}\n\n---设置 bus 音量\n---Sets bus volume.\n---@param volume number 线性音量倍率 / Linear volume multiplier\n---@return boolean ok bus 存在时为 true / true when the bus exists\nfunction AudioBus:set_volume(volume)\nend\n\n---Soluna 主模块\n---Soluna root module.\n---@class soluna\n---@field platform \"windows\"|\"macos\"|\"linux\"|\"wasm\" 当前平台 / Current platform\n---@field version string 运行时版本字符串 / Runtime version string\n---@field version_api integer API 版本号 / API version number\nlocal soluna = {}\n\n---返回 `.game` 设置表\n---Returns the `.game` settings table.\n---@return table settings 游戏设置 / Game settings\nfunction soluna.settings()\nend\n\n---设置窗口标题\n---Sets the window title.\n---@param text string 标题文字 / Window title\nfunction soluna.set_window_title(text)\nend\n\n---设置窗口图标\n---Sets one icon image or an icon image list.\n---@param data soluna.IconImage|soluna.IconImage[] 图标数据 / Icon data\nfunction soluna.set_icon(data)\nend\n\n---返回并创建游戏数据目录\n---Returns and creates the game data directory.\n---@param name? string 项目名，默认来自 `settings.project` / Project name, default from `settings.project`\n---@return string path 绝对路径，结尾带 `/` / Absolute path ending with `/`\nfunction soluna.gamedir(name)\nend\n\n---加载 sprite bundle\n---Loads a sprite bundle.\n---@param filename string|table `.dl` 文件路径或已解析 bundle 表 / `.dl` path or parsed bundle table\n---@return SpriteBundle sprites sprite 名称映射 / Sprite name mapping\nfunction soluna.load_sprites(filename)\nend\n\n---预加载运行时生成的 RGBA sprite 图片\n---Preloads runtime-generated RGBA sprite images.\n---@param sprites soluna.PreloadSprite|soluna.PreloadSprite[] 单个 sprite 或列表 / One sprite or a list\nfunction soluna.preload(sprites)\nend\n\n---加载音频定义 bundle\n---Loads an audio definition bundle.\n---@param filename string `sounds.dl` 文件路径 / `sounds.dl` path\nfunction soluna.load_sounds(filename)\nend\n\n---播放音频并返回 voice\n---Plays a sound and returns a voice handle.\n---@param name string `sounds.dl` 中的音频名 / Sound name from `sounds.dl`\n---@param opts? soluna.AudioPlayOptions 播放选项覆盖 / Playback option overrides\n---@return soluna.AudioVoice? voice voice 句柄 / Voice handle\n---@return string? err 错误信息 / Error message\nfunction soluna.play_sound(name, opts)\nend\n\n---返回 audio bus 句柄\n---Returns an audio bus handle.\n---@param name string bus 名称 / Bus name\n---@return soluna.AudioBus? bus bus 句柄 / Bus handle\n---@return string? err 错误信息 / Error message\nfunction soluna.audio_bus(name)\nend\n\nreturn soluna\n"
  },
  {
    "path": "docs/text.lua",
    "content": "---@meta soluna.text\n\n---文本预处理模块\n---Text preprocessing module.\n---@class soluna.text\n---@field convert table<string, string> 文本转换缓存表 / Text conversion cache table\nlocal text = {}\n\n---初始化内嵌 icon bundle\n---Initializes the embedded icon bundle.\n---@param bundle_file string icon bundle `.dl` 文件路径 / Icon bundle `.dl` path\nfunction text.init(bundle_file)\nend\n\nreturn text\n"
  },
  {
    "path": "docs/url.lua",
    "content": "---@meta soluna.url\n\n---URL 模块\n---URL module.\n---@class soluna.url\nlocal url = {}\n\n---用系统默认浏览器打开 URL\n---Opens a URL with the system default browser.\n---@param link string URL / URL\nfunction url.open(link)\nend\n\nreturn url\n"
  },
  {
    "path": "docs/zip.lua",
    "content": "---@meta soluna.zip\n\n---ZIP 模块\n---ZIP module.\n---@class soluna.zip\nlocal zip = {}\n\n---打开 ZIP 文件\n---Opens a ZIP archive.\n---@param filename string ZIP 文件路径 / ZIP file path\n---@param mode \"r\"|\"w\"|\"a\" 打开模式：读、写、追加 / Open mode: read, write, append\n---@return userdata? zipfile ZIP 句柄；失败时为 nil / ZIP handle, nil on failure\nfunction zip.open(filename, mode)\nend\n\nreturn zip\n"
  },
  {
    "path": "extlua/extlua.c",
    "content": "// AUTO GENERATED by extlua.temp.c, DONT EDIT\n\n#include <lua.h>\n#include <lauxlib.h>\n#include <stdarg.h>\n#include <assert.h>\n#include <stdio.h>\n\nstruct lua_api {\n\tint version;\n\n\tlua_State * (*lua_newstate) (lua_Alloc f, void *ud, unsigned seed);\n\tvoid (*lua_close) (lua_State *L);\n\tlua_State * (*lua_newthread) (lua_State *L);\n\tint (*lua_closethread) (lua_State *L, lua_State *from);\n\tlua_CFunction (*lua_atpanic) (lua_State *L, lua_CFunction panicf);\n\tlua_Number (*lua_version) (lua_State *L);\n\tint (*lua_absindex) (lua_State *L, int idx);\n\tint (*lua_gettop) (lua_State *L);\n\tvoid (*lua_settop) (lua_State *L, int idx);\n\tvoid (*lua_pushvalue) (lua_State *L, int idx);\n\tvoid (*lua_rotate) (lua_State *L, int idx, int n);\n\tvoid (*lua_copy) (lua_State *L, int fromidx, int toidx);\n\tint (*lua_checkstack) (lua_State *L, int n);\n\tvoid (*lua_xmove) (lua_State *from, lua_State *to, int n);\n\tint (*lua_isnumber) (lua_State *L, int idx);\n\tint (*lua_isstring) (lua_State *L, int idx);\n\tint (*lua_iscfunction) (lua_State *L, int idx);\n\tint (*lua_isinteger) (lua_State *L, int idx);\n\tint (*lua_isuserdata) (lua_State *L, int idx);\n\tint (*lua_type) (lua_State *L, int idx);\n\tconst char     * (*lua_typename) (lua_State *L, int tp);\n\tlua_Number (*lua_tonumberx) (lua_State *L, int idx, int *isnum);\n\tlua_Integer (*lua_tointegerx) (lua_State *L, int idx, int *isnum);\n\tint (*lua_toboolean) (lua_State *L, int idx);\n\tconst char     * (*lua_tolstring) (lua_State *L, int idx, size_t *len);\n\tlua_Unsigned (*lua_rawlen) (lua_State *L, int idx);\n\tlua_CFunction (*lua_tocfunction) (lua_State *L, int idx);\n\tvoid\t       * (*lua_touserdata) (lua_State *L, int idx);\n\tlua_State      * (*lua_tothread) (lua_State *L, int idx);\n\tconst void     * (*lua_topointer) (lua_State *L, int idx);\n\tvoid (*lua_arith) (lua_State *L, int op);\n\tint (*lua_rawequal) (lua_State *L, int idx1, int idx2);\n\tint (*lua_compare) (lua_State *L, int idx1, int idx2, int op);\n\tvoid (*lua_pushnil) (lua_State *L);\n\tvoid (*lua_pushnumber) (lua_State *L, lua_Number n);\n\tvoid (*lua_pushinteger) (lua_State *L, lua_Integer n);\n\tconst char * (*lua_pushlstring) (lua_State *L, const char *s, size_t len);\n\tconst char * (*lua_pushexternalstring) (lua_State *L,\n\t\tconst char *s, size_t len, lua_Alloc falloc, void *ud);\n\tconst char * (*lua_pushstring) (lua_State *L, const char *s);\n\tconst char * (*lua_pushvfstring) (lua_State *L, const char *fmt,\n                                                      va_list argp);\n\tvoid (*lua_pushcclosure) (lua_State *L, lua_CFunction fn, int n);\n\tvoid (*lua_pushboolean) (lua_State *L, int b);\n\tvoid (*lua_pushlightuserdata) (lua_State *L, void *p);\n\tint (*lua_pushthread) (lua_State *L);\n\tint (*lua_getglobal) (lua_State *L, const char *name);\n\tint (*lua_gettable) (lua_State *L, int idx);\n\tint (*lua_getfield) (lua_State *L, int idx, const char *k);\n\tint (*lua_geti) (lua_State *L, int idx, lua_Integer n);\n\tint (*lua_rawget) (lua_State *L, int idx);\n\tint (*lua_rawgeti) (lua_State *L, int idx, lua_Integer n);\n\tint (*lua_rawgetp) (lua_State *L, int idx, const void *p);\n\tvoid (*lua_createtable) (lua_State *L, int narr, int nrec);\n\tvoid * (*lua_newuserdatauv) (lua_State *L, size_t sz, int nuvalue);\n\tint (*lua_getmetatable) (lua_State *L, int objindex);\n\tint (*lua_getiuservalue) (lua_State *L, int idx, int n);\n\tvoid (*lua_setglobal) (lua_State *L, const char *name);\n\tvoid (*lua_settable) (lua_State *L, int idx);\n\tvoid (*lua_setfield) (lua_State *L, int idx, const char *k);\n\tvoid (*lua_seti) (lua_State *L, int idx, lua_Integer n);\n\tvoid (*lua_rawset) (lua_State *L, int idx);\n\tvoid (*lua_rawseti) (lua_State *L, int idx, lua_Integer n);\n\tvoid (*lua_rawsetp) (lua_State *L, int idx, const void *p);\n\tint (*lua_setmetatable) (lua_State *L, int objindex);\n\tint (*lua_setiuservalue) (lua_State *L, int idx, int n);\n\tvoid (*lua_callk) (lua_State *L, int nargs, int nresults,\n                           lua_KContext ctx, lua_KFunction k);\n\tint (*lua_pcallk) (lua_State *L, int nargs, int nresults, int errfunc,\n                            lua_KContext ctx, lua_KFunction k);\n\tint (*lua_load) (lua_State *L, lua_Reader reader, void *dt,\n                          const char *chunkname, const char *mode);\n\tint (*lua_dump) (lua_State *L, lua_Writer writer, void *data, int strip);\n\tint (*lua_yieldk) (lua_State *L, int nresults, lua_KContext ctx,\n                               lua_KFunction k);\n\tint (*lua_resume) (lua_State *L, lua_State *from, int narg,\n                               int *nres);\n\tint (*lua_status) (lua_State *L);\n\tint (*lua_isyieldable) (lua_State *L);\n\tvoid (*lua_setwarnf) (lua_State *L, lua_WarnFunction f, void *ud);\n\tvoid (*lua_warning) (lua_State *L, const char *msg, int tocont);\n\tint (*lua_error) (lua_State *L);\n\tint (*lua_next) (lua_State *L, int idx);\n\tvoid (*lua_concat) (lua_State *L, int n);\n\tvoid (*lua_len) (lua_State *L, int idx);\n\tunsigned (*lua_numbertocstring) (lua_State *L, int idx, char *buff);\n\tsize_t (*lua_stringtonumber) (lua_State *L, const char *s);\n\tlua_Alloc (*lua_getallocf) (lua_State *L, void **ud);\n\tvoid (*lua_setallocf) (lua_State *L, lua_Alloc f, void *ud);\n\tvoid (*lua_toclose) (lua_State *L, int idx);\n\tvoid (*lua_closeslot) (lua_State *L, int idx);\n\tint (*lua_getstack) (lua_State *L, int level, lua_Debug *ar);\n\tint (*lua_getinfo) (lua_State *L, const char *what, lua_Debug *ar);\n\tconst char * (*lua_getlocal) (lua_State *L, const lua_Debug *ar, int n);\n\tconst char * (*lua_setlocal) (lua_State *L, const lua_Debug *ar, int n);\n\tconst char * (*lua_getupvalue) (lua_State *L, int funcindex, int n);\n\tconst char * (*lua_setupvalue) (lua_State *L, int funcindex, int n);\n\tvoid * (*lua_upvalueid) (lua_State *L, int fidx, int n);\n\tvoid (*lua_upvaluejoin) (lua_State *L, int fidx1, int n1,\n                                               int fidx2, int n2);\n\tvoid (*lua_sethook) (lua_State *L, lua_Hook func, int mask, int count);\n\tlua_Hook (*lua_gethook) (lua_State *L);\n\tint (*lua_gethookmask) (lua_State *L);\n\tint (*lua_gethookcount) (lua_State *L);\n\tvoid (*luaL_checkversion_) (lua_State *L, lua_Number ver, size_t sz);\n\tint (*luaL_getmetafield) (lua_State *L, int obj, const char *e);\n\tint (*luaL_callmeta) (lua_State *L, int obj, const char *e);\n\tconst char * (*luaL_tolstring) (lua_State *L, int idx, size_t *len);\n\tint (*luaL_argerror) (lua_State *L, int arg, const char *extramsg);\n\tint (*luaL_typeerror) (lua_State *L, int arg, const char *tname);\n\tconst char * (*luaL_checklstring) (lua_State *L, int arg,\n                                                          size_t *l);\n\tconst char * (*luaL_optlstring) (lua_State *L, int arg,\n                                          const char *def, size_t *l);\n\tlua_Number (*luaL_checknumber) (lua_State *L, int arg);\n\tlua_Number (*luaL_optnumber) (lua_State *L, int arg, lua_Number def);\n\tlua_Integer (*luaL_checkinteger) (lua_State *L, int arg);\n\tlua_Integer (*luaL_optinteger) (lua_State *L, int arg,\n                                          lua_Integer def);\n\tvoid (*luaL_checkstack) (lua_State *L, int sz, const char *msg);\n\tvoid (*luaL_checktype) (lua_State *L, int arg, int t);\n\tvoid (*luaL_checkany) (lua_State *L, int arg);\n\tint (*luaL_newmetatable) (lua_State *L, const char *tname);\n\tvoid (*luaL_setmetatable) (lua_State *L, const char *tname);\n\tvoid * (*luaL_testudata) (lua_State *L, int ud, const char *tname);\n\tvoid * (*luaL_checkudata) (lua_State *L, int ud, const char *tname);\n\tvoid (*luaL_where) (lua_State *L, int lvl);\n\tint (*luaL_checkoption) (lua_State *L, int arg, const char *def,\n                                   const char *const lst[]);\n\tint (*luaL_fileresult) (lua_State *L, int stat, const char *fname);\n\tint (*luaL_execresult) (lua_State *L, int stat);\n\tvoid * (*luaL_alloc) (void *ud, void *ptr, size_t osize,\n                                                    size_t nsize);\n\tint (*luaL_ref) (lua_State *L, int t);\n\tvoid (*luaL_unref) (lua_State *L, int t, int ref);\n\tint (*luaL_loadfilex) (lua_State *L, const char *filename,\n                                               const char *mode);\n\tint (*luaL_loadbufferx) (lua_State *L, const char *buff, size_t sz,\n                                   const char *name, const char *mode);\n\tint (*luaL_loadstring) (lua_State *L, const char *s);\n\tlua_State * (*luaL_newstate) (void);\n\tunsigned (*luaL_makeseed) (lua_State *L);\n\tlua_Integer (*luaL_len) (lua_State *L, int idx);\n\tvoid (*luaL_addgsub) (luaL_Buffer *b, const char *s,\n                                     const char *p, const char *r);\n\tconst char * (*luaL_gsub) (lua_State *L, const char *s,\n                                    const char *p, const char *r);\n\tvoid (*luaL_setfuncs) (lua_State *L, const luaL_Reg *l, int nup);\n\tint (*luaL_getsubtable) (lua_State *L, int idx, const char *fname);\n\tvoid (*luaL_traceback) (lua_State *L, lua_State *L1,\n                                  const char *msg, int level);\n\tvoid (*luaL_requiref) (lua_State *L, const char *modname,\n                                 lua_CFunction openf, int glb);\n\tvoid (*luaL_buffinit) (lua_State *L, luaL_Buffer *B);\n\tchar * (*luaL_prepbuffsize) (luaL_Buffer *B, size_t sz);\n\tvoid (*luaL_addlstring) (luaL_Buffer *B, const char *s, size_t l);\n\tvoid (*luaL_addstring) (luaL_Buffer *B, const char *s);\n\tvoid (*luaL_addvalue) (luaL_Buffer *B);\n\tvoid (*luaL_pushresult) (luaL_Buffer *B);\n\tvoid (*luaL_pushresultsize) (luaL_Buffer *B, size_t sz);\n\tchar * (*luaL_buffinitsize) (lua_State *L, luaL_Buffer *B, size_t sz);\n\tint (*lua_gc) (lua_State *L, int what, ...);\n};\n\nstatic struct lua_api API;\n\nLUA_API lua_State *\nlua_newstate(lua_Alloc f, void *ud, unsigned seed) {\n\treturn API.lua_newstate(f,ud,seed);\n}\n\nLUA_API void\nlua_close(lua_State *L) {\n\tAPI.lua_close(L);\n}\n\nLUA_API lua_State *\nlua_newthread(lua_State *L) {\n\treturn API.lua_newthread(L);\n}\n\nLUA_API int\nlua_closethread(lua_State *L, lua_State *from) {\n\treturn API.lua_closethread(L,from);\n}\n\nLUA_API lua_CFunction\nlua_atpanic(lua_State *L, lua_CFunction panicf) {\n\treturn API.lua_atpanic(L,panicf);\n}\n\nLUA_API lua_Number\nlua_version(lua_State *L) {\n\treturn API.lua_version(L);\n}\n\nLUA_API int\nlua_absindex(lua_State *L, int idx) {\n\treturn API.lua_absindex(L,idx);\n}\n\nLUA_API int\nlua_gettop(lua_State *L) {\n\treturn API.lua_gettop(L);\n}\n\nLUA_API void\nlua_settop(lua_State *L, int idx) {\n\tAPI.lua_settop(L,idx);\n}\n\nLUA_API void\nlua_pushvalue(lua_State *L, int idx) {\n\tAPI.lua_pushvalue(L,idx);\n}\n\nLUA_API void\nlua_rotate(lua_State *L, int idx, int n) {\n\tAPI.lua_rotate(L,idx,n);\n}\n\nLUA_API void\nlua_copy(lua_State *L, int fromidx, int toidx) {\n\tAPI.lua_copy(L,fromidx,toidx);\n}\n\nLUA_API int\nlua_checkstack(lua_State *L, int n) {\n\treturn API.lua_checkstack(L,n);\n}\n\nLUA_API void\nlua_xmove(lua_State *from, lua_State *to, int n) {\n\tAPI.lua_xmove(from,to,n);\n}\n\nLUA_API int\nlua_isnumber(lua_State *L, int idx) {\n\treturn API.lua_isnumber(L,idx);\n}\n\nLUA_API int\nlua_isstring(lua_State *L, int idx) {\n\treturn API.lua_isstring(L,idx);\n}\n\nLUA_API int\nlua_iscfunction(lua_State *L, int idx) {\n\treturn API.lua_iscfunction(L,idx);\n}\n\nLUA_API int\nlua_isinteger(lua_State *L, int idx) {\n\treturn API.lua_isinteger(L,idx);\n}\n\nLUA_API int\nlua_isuserdata(lua_State *L, int idx) {\n\treturn API.lua_isuserdata(L,idx);\n}\n\nLUA_API int\nlua_type(lua_State *L, int idx) {\n\treturn API.lua_type(L,idx);\n}\n\nLUA_API const char     *\nlua_typename(lua_State *L, int tp) {\n\treturn API.lua_typename(L,tp);\n}\n\nLUA_API lua_Number\nlua_tonumberx(lua_State *L, int idx, int *isnum) {\n\treturn API.lua_tonumberx(L,idx,isnum);\n}\n\nLUA_API lua_Integer\nlua_tointegerx(lua_State *L, int idx, int *isnum) {\n\treturn API.lua_tointegerx(L,idx,isnum);\n}\n\nLUA_API int\nlua_toboolean(lua_State *L, int idx) {\n\treturn API.lua_toboolean(L,idx);\n}\n\nLUA_API const char     *\nlua_tolstring(lua_State *L, int idx, size_t *len) {\n\treturn API.lua_tolstring(L,idx,len);\n}\n\nLUA_API lua_Unsigned\nlua_rawlen(lua_State *L, int idx) {\n\treturn API.lua_rawlen(L,idx);\n}\n\nLUA_API lua_CFunction\nlua_tocfunction(lua_State *L, int idx) {\n\treturn API.lua_tocfunction(L,idx);\n}\n\nLUA_API void\t       *\nlua_touserdata(lua_State *L, int idx) {\n\treturn API.lua_touserdata(L,idx);\n}\n\nLUA_API lua_State      *\nlua_tothread(lua_State *L, int idx) {\n\treturn API.lua_tothread(L,idx);\n}\n\nLUA_API const void     *\nlua_topointer(lua_State *L, int idx) {\n\treturn API.lua_topointer(L,idx);\n}\n\nLUA_API void\nlua_arith(lua_State *L, int op) {\n\tAPI.lua_arith(L,op);\n}\n\nLUA_API int\nlua_rawequal(lua_State *L, int idx1, int idx2) {\n\treturn API.lua_rawequal(L,idx1,idx2);\n}\n\nLUA_API int\nlua_compare(lua_State *L, int idx1, int idx2, int op) {\n\treturn API.lua_compare(L,idx1,idx2,op);\n}\n\nLUA_API void\nlua_pushnil(lua_State *L) {\n\tAPI.lua_pushnil(L);\n}\n\nLUA_API void\nlua_pushnumber(lua_State *L, lua_Number n) {\n\tAPI.lua_pushnumber(L,n);\n}\n\nLUA_API void\nlua_pushinteger(lua_State *L, lua_Integer n) {\n\tAPI.lua_pushinteger(L,n);\n}\n\nLUA_API const char *\nlua_pushlstring(lua_State *L, const char *s, size_t len) {\n\treturn API.lua_pushlstring(L,s,len);\n}\n\nLUA_API const char *\nlua_pushexternalstring(lua_State *L,\n\t\tconst char *s, size_t len, lua_Alloc falloc, void *ud) {\n\treturn API.lua_pushexternalstring(L,s,len,falloc,ud);\n}\n\nLUA_API const char *\nlua_pushstring(lua_State *L, const char *s) {\n\treturn API.lua_pushstring(L,s);\n}\n\nLUA_API const char *\nlua_pushvfstring(lua_State *L, const char *fmt,\n                                                      va_list argp) {\n\treturn API.lua_pushvfstring(L,fmt,argp);\n}\n\nLUA_API void\nlua_pushcclosure(lua_State *L, lua_CFunction fn, int n) {\n\tAPI.lua_pushcclosure(L,fn,n);\n}\n\nLUA_API void\nlua_pushboolean(lua_State *L, int b) {\n\tAPI.lua_pushboolean(L,b);\n}\n\nLUA_API void\nlua_pushlightuserdata(lua_State *L, void *p) {\n\tAPI.lua_pushlightuserdata(L,p);\n}\n\nLUA_API int\nlua_pushthread(lua_State *L) {\n\treturn API.lua_pushthread(L);\n}\n\nLUA_API int\nlua_getglobal(lua_State *L, const char *name) {\n\treturn API.lua_getglobal(L,name);\n}\n\nLUA_API int\nlua_gettable(lua_State *L, int idx) {\n\treturn API.lua_gettable(L,idx);\n}\n\nLUA_API int\nlua_getfield(lua_State *L, int idx, const char *k) {\n\treturn API.lua_getfield(L,idx,k);\n}\n\nLUA_API int\nlua_geti(lua_State *L, int idx, lua_Integer n) {\n\treturn API.lua_geti(L,idx,n);\n}\n\nLUA_API int\nlua_rawget(lua_State *L, int idx) {\n\treturn API.lua_rawget(L,idx);\n}\n\nLUA_API int\nlua_rawgeti(lua_State *L, int idx, lua_Integer n) {\n\treturn API.lua_rawgeti(L,idx,n);\n}\n\nLUA_API int\nlua_rawgetp(lua_State *L, int idx, const void *p) {\n\treturn API.lua_rawgetp(L,idx,p);\n}\n\nLUA_API void\nlua_createtable(lua_State *L, int narr, int nrec) {\n\tAPI.lua_createtable(L,narr,nrec);\n}\n\nLUA_API void *\nlua_newuserdatauv(lua_State *L, size_t sz, int nuvalue) {\n\treturn API.lua_newuserdatauv(L,sz,nuvalue);\n}\n\nLUA_API int\nlua_getmetatable(lua_State *L, int objindex) {\n\treturn API.lua_getmetatable(L,objindex);\n}\n\nLUA_API int\nlua_getiuservalue(lua_State *L, int idx, int n) {\n\treturn API.lua_getiuservalue(L,idx,n);\n}\n\nLUA_API void\nlua_setglobal(lua_State *L, const char *name) {\n\tAPI.lua_setglobal(L,name);\n}\n\nLUA_API void\nlua_settable(lua_State *L, int idx) {\n\tAPI.lua_settable(L,idx);\n}\n\nLUA_API void\nlua_setfield(lua_State *L, int idx, const char *k) {\n\tAPI.lua_setfield(L,idx,k);\n}\n\nLUA_API void\nlua_seti(lua_State *L, int idx, lua_Integer n) {\n\tAPI.lua_seti(L,idx,n);\n}\n\nLUA_API void\nlua_rawset(lua_State *L, int idx) {\n\tAPI.lua_rawset(L,idx);\n}\n\nLUA_API void\nlua_rawseti(lua_State *L, int idx, lua_Integer n) {\n\tAPI.lua_rawseti(L,idx,n);\n}\n\nLUA_API void\nlua_rawsetp(lua_State *L, int idx, const void *p) {\n\tAPI.lua_rawsetp(L,idx,p);\n}\n\nLUA_API int\nlua_setmetatable(lua_State *L, int objindex) {\n\treturn API.lua_setmetatable(L,objindex);\n}\n\nLUA_API int\nlua_setiuservalue(lua_State *L, int idx, int n) {\n\treturn API.lua_setiuservalue(L,idx,n);\n}\n\nLUA_API void\nlua_callk(lua_State *L, int nargs, int nresults,\n                           lua_KContext ctx, lua_KFunction k) {\n\tAPI.lua_callk(L,nargs,nresults,ctx,k);\n}\n\nLUA_API int\nlua_pcallk(lua_State *L, int nargs, int nresults, int errfunc,\n                            lua_KContext ctx, lua_KFunction k) {\n\treturn API.lua_pcallk(L,nargs,nresults,errfunc,ctx,k);\n}\n\nLUA_API int\nlua_load(lua_State *L, lua_Reader reader, void *dt,\n                          const char *chunkname, const char *mode) {\n\treturn API.lua_load(L,reader,dt,chunkname,mode);\n}\n\nLUA_API int\nlua_dump(lua_State *L, lua_Writer writer, void *data, int strip) {\n\treturn API.lua_dump(L,writer,data,strip);\n}\n\nLUA_API int\nlua_yieldk(lua_State *L, int nresults, lua_KContext ctx,\n                               lua_KFunction k) {\n\treturn API.lua_yieldk(L,nresults,ctx,k);\n}\n\nLUA_API int\nlua_resume(lua_State *L, lua_State *from, int narg,\n                               int *nres) {\n\treturn API.lua_resume(L,from,narg,nres);\n}\n\nLUA_API int\nlua_status(lua_State *L) {\n\treturn API.lua_status(L);\n}\n\nLUA_API int\nlua_isyieldable(lua_State *L) {\n\treturn API.lua_isyieldable(L);\n}\n\nLUA_API void\nlua_setwarnf(lua_State *L, lua_WarnFunction f, void *ud) {\n\tAPI.lua_setwarnf(L,f,ud);\n}\n\nLUA_API void\nlua_warning(lua_State *L, const char *msg, int tocont) {\n\tAPI.lua_warning(L,msg,tocont);\n}\n\nLUA_API int\nlua_error(lua_State *L) {\n\treturn API.lua_error(L);\n}\n\nLUA_API int\nlua_next(lua_State *L, int idx) {\n\treturn API.lua_next(L,idx);\n}\n\nLUA_API void\nlua_concat(lua_State *L, int n) {\n\tAPI.lua_concat(L,n);\n}\n\nLUA_API void\nlua_len(lua_State *L, int idx) {\n\tAPI.lua_len(L,idx);\n}\n\nLUA_API unsigned\nlua_numbertocstring(lua_State *L, int idx, char *buff) {\n\treturn API.lua_numbertocstring(L,idx,buff);\n}\n\nLUA_API size_t\nlua_stringtonumber(lua_State *L, const char *s) {\n\treturn API.lua_stringtonumber(L,s);\n}\n\nLUA_API lua_Alloc\nlua_getallocf(lua_State *L, void **ud) {\n\treturn API.lua_getallocf(L,ud);\n}\n\nLUA_API void\nlua_setallocf(lua_State *L, lua_Alloc f, void *ud) {\n\tAPI.lua_setallocf(L,f,ud);\n}\n\nLUA_API void\nlua_toclose(lua_State *L, int idx) {\n\tAPI.lua_toclose(L,idx);\n}\n\nLUA_API void\nlua_closeslot(lua_State *L, int idx) {\n\tAPI.lua_closeslot(L,idx);\n}\n\nLUA_API int\nlua_getstack(lua_State *L, int level, lua_Debug *ar) {\n\treturn API.lua_getstack(L,level,ar);\n}\n\nLUA_API int\nlua_getinfo(lua_State *L, const char *what, lua_Debug *ar) {\n\treturn API.lua_getinfo(L,what,ar);\n}\n\nLUA_API const char *\nlua_getlocal(lua_State *L, const lua_Debug *ar, int n) {\n\treturn API.lua_getlocal(L,ar,n);\n}\n\nLUA_API const char *\nlua_setlocal(lua_State *L, const lua_Debug *ar, int n) {\n\treturn API.lua_setlocal(L,ar,n);\n}\n\nLUA_API const char *\nlua_getupvalue(lua_State *L, int funcindex, int n) {\n\treturn API.lua_getupvalue(L,funcindex,n);\n}\n\nLUA_API const char *\nlua_setupvalue(lua_State *L, int funcindex, int n) {\n\treturn API.lua_setupvalue(L,funcindex,n);\n}\n\nLUA_API void *\nlua_upvalueid(lua_State *L, int fidx, int n) {\n\treturn API.lua_upvalueid(L,fidx,n);\n}\n\nLUA_API void\nlua_upvaluejoin(lua_State *L, int fidx1, int n1,\n                                               int fidx2, int n2) {\n\tAPI.lua_upvaluejoin(L,fidx1,n1,fidx2,n2);\n}\n\nLUA_API void\nlua_sethook(lua_State *L, lua_Hook func, int mask, int count) {\n\tAPI.lua_sethook(L,func,mask,count);\n}\n\nLUA_API lua_Hook\nlua_gethook(lua_State *L) {\n\treturn API.lua_gethook(L);\n}\n\nLUA_API int\nlua_gethookmask(lua_State *L) {\n\treturn API.lua_gethookmask(L);\n}\n\nLUA_API int\nlua_gethookcount(lua_State *L) {\n\treturn API.lua_gethookcount(L);\n}\n\nLUA_API void\nluaL_checkversion_(lua_State *L, lua_Number ver, size_t sz) {\n\tAPI.luaL_checkversion_(L,ver,sz);\n}\n\nLUA_API int\nluaL_getmetafield(lua_State *L, int obj, const char *e) {\n\treturn API.luaL_getmetafield(L,obj,e);\n}\n\nLUA_API int\nluaL_callmeta(lua_State *L, int obj, const char *e) {\n\treturn API.luaL_callmeta(L,obj,e);\n}\n\nLUA_API const char *\nluaL_tolstring(lua_State *L, int idx, size_t *len) {\n\treturn API.luaL_tolstring(L,idx,len);\n}\n\nLUA_API int\nluaL_argerror(lua_State *L, int arg, const char *extramsg) {\n\treturn API.luaL_argerror(L,arg,extramsg);\n}\n\nLUA_API int\nluaL_typeerror(lua_State *L, int arg, const char *tname) {\n\treturn API.luaL_typeerror(L,arg,tname);\n}\n\nLUA_API const char *\nluaL_checklstring(lua_State *L, int arg,\n                                                          size_t *l) {\n\treturn API.luaL_checklstring(L,arg,l);\n}\n\nLUA_API const char *\nluaL_optlstring(lua_State *L, int arg,\n                                          const char *def, size_t *l) {\n\treturn API.luaL_optlstring(L,arg,def,l);\n}\n\nLUA_API lua_Number\nluaL_checknumber(lua_State *L, int arg) {\n\treturn API.luaL_checknumber(L,arg);\n}\n\nLUA_API lua_Number\nluaL_optnumber(lua_State *L, int arg, lua_Number def) {\n\treturn API.luaL_optnumber(L,arg,def);\n}\n\nLUA_API lua_Integer\nluaL_checkinteger(lua_State *L, int arg) {\n\treturn API.luaL_checkinteger(L,arg);\n}\n\nLUA_API lua_Integer\nluaL_optinteger(lua_State *L, int arg,\n                                          lua_Integer def) {\n\treturn API.luaL_optinteger(L,arg,def);\n}\n\nLUA_API void\nluaL_checkstack(lua_State *L, int sz, const char *msg) {\n\tAPI.luaL_checkstack(L,sz,msg);\n}\n\nLUA_API void\nluaL_checktype(lua_State *L, int arg, int t) {\n\tAPI.luaL_checktype(L,arg,t);\n}\n\nLUA_API void\nluaL_checkany(lua_State *L, int arg) {\n\tAPI.luaL_checkany(L,arg);\n}\n\nLUA_API int\nluaL_newmetatable(lua_State *L, const char *tname) {\n\treturn API.luaL_newmetatable(L,tname);\n}\n\nLUA_API void\nluaL_setmetatable(lua_State *L, const char *tname) {\n\tAPI.luaL_setmetatable(L,tname);\n}\n\nLUA_API void *\nluaL_testudata(lua_State *L, int ud, const char *tname) {\n\treturn API.luaL_testudata(L,ud,tname);\n}\n\nLUA_API void *\nluaL_checkudata(lua_State *L, int ud, const char *tname) {\n\treturn API.luaL_checkudata(L,ud,tname);\n}\n\nLUA_API void\nluaL_where(lua_State *L, int lvl) {\n\tAPI.luaL_where(L,lvl);\n}\n\nLUA_API int\nluaL_checkoption(lua_State *L, int arg, const char *def,\n                                   const char *const lst[]) {\n\treturn API.luaL_checkoption(L,arg,def,lst);\n}\n\nLUA_API int\nluaL_fileresult(lua_State *L, int stat, const char *fname) {\n\treturn API.luaL_fileresult(L,stat,fname);\n}\n\nLUA_API int\nluaL_execresult(lua_State *L, int stat) {\n\treturn API.luaL_execresult(L,stat);\n}\n\nLUA_API void *\nluaL_alloc(void *ud, void *ptr, size_t osize,\n                                                    size_t nsize) {\n\treturn API.luaL_alloc(ud,ptr,osize,nsize);\n}\n\nLUA_API int\nluaL_ref(lua_State *L, int t) {\n\treturn API.luaL_ref(L,t);\n}\n\nLUA_API void\nluaL_unref(lua_State *L, int t, int ref) {\n\tAPI.luaL_unref(L,t,ref);\n}\n\nLUA_API int\nluaL_loadfilex(lua_State *L, const char *filename,\n                                               const char *mode) {\n\treturn API.luaL_loadfilex(L,filename,mode);\n}\n\nLUA_API int\nluaL_loadbufferx(lua_State *L, const char *buff, size_t sz,\n                                   const char *name, const char *mode) {\n\treturn API.luaL_loadbufferx(L,buff,sz,name,mode);\n}\n\nLUA_API int\nluaL_loadstring(lua_State *L, const char *s) {\n\treturn API.luaL_loadstring(L,s);\n}\n\nLUA_API lua_State *\nluaL_newstate(void) {\n\treturn API.luaL_newstate();\n}\n\nLUA_API unsigned\nluaL_makeseed(lua_State *L) {\n\treturn API.luaL_makeseed(L);\n}\n\nLUA_API lua_Integer\nluaL_len(lua_State *L, int idx) {\n\treturn API.luaL_len(L,idx);\n}\n\nLUA_API void\nluaL_addgsub(luaL_Buffer *b, const char *s,\n                                     const char *p, const char *r) {\n\tAPI.luaL_addgsub(b,s,p,r);\n}\n\nLUA_API const char *\nluaL_gsub(lua_State *L, const char *s,\n                                    const char *p, const char *r) {\n\treturn API.luaL_gsub(L,s,p,r);\n}\n\nLUA_API void\nluaL_setfuncs(lua_State *L, const luaL_Reg *l, int nup) {\n\tAPI.luaL_setfuncs(L,l,nup);\n}\n\nLUA_API int\nluaL_getsubtable(lua_State *L, int idx, const char *fname) {\n\treturn API.luaL_getsubtable(L,idx,fname);\n}\n\nLUA_API void\nluaL_traceback(lua_State *L, lua_State *L1,\n                                  const char *msg, int level) {\n\tAPI.luaL_traceback(L,L1,msg,level);\n}\n\nLUA_API void\nluaL_requiref(lua_State *L, const char *modname,\n                                 lua_CFunction openf, int glb) {\n\tAPI.luaL_requiref(L,modname,openf,glb);\n}\n\nLUA_API void\nluaL_buffinit(lua_State *L, luaL_Buffer *B) {\n\tAPI.luaL_buffinit(L,B);\n}\n\nLUA_API char *\nluaL_prepbuffsize(luaL_Buffer *B, size_t sz) {\n\treturn API.luaL_prepbuffsize(B,sz);\n}\n\nLUA_API void\nluaL_addlstring(luaL_Buffer *B, const char *s, size_t l) {\n\tAPI.luaL_addlstring(B,s,l);\n}\n\nLUA_API void\nluaL_addstring(luaL_Buffer *B, const char *s) {\n\tAPI.luaL_addstring(B,s);\n}\n\nLUA_API void\nluaL_addvalue(luaL_Buffer *B) {\n\tAPI.luaL_addvalue(B);\n}\n\nLUA_API void\nluaL_pushresult(luaL_Buffer *B) {\n\tAPI.luaL_pushresult(B);\n}\n\nLUA_API void\nluaL_pushresultsize(luaL_Buffer *B, size_t sz) {\n\tAPI.luaL_pushresultsize(B,sz);\n}\n\nLUA_API char *\nluaL_buffinitsize(lua_State *L, luaL_Buffer *B, size_t sz) {\n\treturn API.luaL_buffinitsize(L,B,sz);\n}\n\n\n\nLUA_API\nconst char *lua_pushfstring (lua_State *L, const char *fmt, ...) {\n\tconst char *ret;\n\tva_list argp;\n\tva_start(argp, fmt);\n\tret = API.lua_pushvfstring(L, fmt, argp);\n\tva_end(argp);\n\treturn ret;\n}\n\nLUA_API int\nlua_gc (lua_State *L, int what, ...) {\n\tva_list argp;\n\tva_start(argp, what);\n\tint p1 = va_arg(argp, int);\n\tint p2 = va_arg(argp, int);\n\tint p3 = va_arg(argp, int);\n\tva_end(argp);\n\treturn API.lua_gc(L, what, p1, p2, p3);\n}\n\nLUA_API int\nluaL_error(lua_State *L, const char *fmt, ...) {\n\tva_list argp;\n\tva_start(argp, fmt);\n\tluaL_where(L, 1);\n\tlua_pushvfstring(L, fmt, argp);\n\tva_end(argp);\n\tlua_concat(L, 2);\n\treturn lua_error(L);\n}\n\nstatic void\nstub_luaL_checkversion_ (lua_State *L, lua_Number ver, size_t sz) {\n}\n\nstatic void stub_lua_createtable (lua_State *L, int narr, int nrec) {\n}\n\nstatic void stub_luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) {\n}\n\nstruct sokol_api;\nstruct soluna_api;\n\nstruct extlua_apis {\n\tstruct lua_api * lua;\n\tstruct sokol_api * sokol;\n\tstruct soluna_api * soluna;\n};\n\nLUA_API void\nluaapi_init(lua_State *L) {\n\tstruct extlua_apis *apis = *(struct extlua_apis **)lua_getextraspace(L);\n\tstruct lua_api * api = apis->lua;\n\tif (api->version == LUA_VERSION_NUM) {\n\t\tAPI = *api;\n\t\treturn;\n\t}\n\t// stub for luaL_newlib\n\tAPI.luaL_checkversion_ = stub_luaL_checkversion_;\n\tAPI.lua_createtable = stub_lua_createtable;\n\tAPI.luaL_setfuncs = stub_luaL_setfuncs;\n}\n"
  },
  {
    "path": "extlua/extlua.temp.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n#include <stdarg.h>\n#include <assert.h>\n#include <stdio.h>\n\nstruct lua_api {\n\tint version;\n\n$API_DECL$\n};\n\nstatic struct lua_api API;\n\n$API_IMPL$\n\nLUA_API\nconst char *lua_pushfstring (lua_State *L, const char *fmt, ...) {\n\tconst char *ret;\n\tva_list argp;\n\tva_start(argp, fmt);\n\tret = API.lua_pushvfstring(L, fmt, argp);\n\tva_end(argp);\n\treturn ret;\n}\n\nLUA_API int\nlua_gc (lua_State *L, int what, ...) {\n\tva_list argp;\n\tva_start(argp, what);\n\tint p1 = va_arg(argp, int);\n\tint p2 = va_arg(argp, int);\n\tint p3 = va_arg(argp, int);\n\tva_end(argp);\n\treturn API.lua_gc(L, what, p1, p2, p3);\n}\n\nLUA_API int\nluaL_error(lua_State *L, const char *fmt, ...) {\n\tva_list argp;\n\tva_start(argp, fmt);\n\tluaL_where(L, 1);\n\tlua_pushvfstring(L, fmt, argp);\n\tva_end(argp);\n\tlua_concat(L, 2);\n\treturn lua_error(L);\n}\n\nstatic void\nstub_luaL_checkversion_ (lua_State *L, lua_Number ver, size_t sz) {\n}\n\nstatic void stub_lua_createtable (lua_State *L, int narr, int nrec) {\n}\n\nstatic void stub_luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) {\n}\n\nstruct sokol_api;\nstruct soluna_api;\n\nstruct extlua_apis {\n\tstruct lua_api * lua;\n\tstruct sokol_api * sokol;\n\tstruct soluna_api * soluna;\n};\n\nLUA_API void\nluaapi_init(lua_State *L) {\n\tstruct extlua_apis *apis = *(struct extlua_apis **)lua_getextraspace(L);\n\tstruct lua_api * api = apis->lua;\n\tif (api->version == LUA_VERSION_NUM) {\n\t\tAPI = *api;\n\t\treturn;\n\t}\n\t// stub for luaL_newlib\n\tAPI.luaL_checkversion_ = stub_luaL_checkversion_;\n\tAPI.lua_createtable = stub_lua_createtable;\n\tAPI.luaL_setfuncs = stub_luaL_setfuncs;\n}\n"
  },
  {
    "path": "extlua/extlua_impl.c",
    "content": "// AUTO GENERATED by extlua_impl.temp.c, DONT EDIT\n\n#include <lua.h>\n#include <lauxlib.h>\n\nstruct lua_api {\n\tint version;\n\n\tlua_State * (*lua_newstate) (lua_Alloc f, void *ud, unsigned seed);\n\tvoid (*lua_close) (lua_State *L);\n\tlua_State * (*lua_newthread) (lua_State *L);\n\tint (*lua_closethread) (lua_State *L, lua_State *from);\n\tlua_CFunction (*lua_atpanic) (lua_State *L, lua_CFunction panicf);\n\tlua_Number (*lua_version) (lua_State *L);\n\tint (*lua_absindex) (lua_State *L, int idx);\n\tint (*lua_gettop) (lua_State *L);\n\tvoid (*lua_settop) (lua_State *L, int idx);\n\tvoid (*lua_pushvalue) (lua_State *L, int idx);\n\tvoid (*lua_rotate) (lua_State *L, int idx, int n);\n\tvoid (*lua_copy) (lua_State *L, int fromidx, int toidx);\n\tint (*lua_checkstack) (lua_State *L, int n);\n\tvoid (*lua_xmove) (lua_State *from, lua_State *to, int n);\n\tint (*lua_isnumber) (lua_State *L, int idx);\n\tint (*lua_isstring) (lua_State *L, int idx);\n\tint (*lua_iscfunction) (lua_State *L, int idx);\n\tint (*lua_isinteger) (lua_State *L, int idx);\n\tint (*lua_isuserdata) (lua_State *L, int idx);\n\tint (*lua_type) (lua_State *L, int idx);\n\tconst char     * (*lua_typename) (lua_State *L, int tp);\n\tlua_Number (*lua_tonumberx) (lua_State *L, int idx, int *isnum);\n\tlua_Integer (*lua_tointegerx) (lua_State *L, int idx, int *isnum);\n\tint (*lua_toboolean) (lua_State *L, int idx);\n\tconst char     * (*lua_tolstring) (lua_State *L, int idx, size_t *len);\n\tlua_Unsigned (*lua_rawlen) (lua_State *L, int idx);\n\tlua_CFunction (*lua_tocfunction) (lua_State *L, int idx);\n\tvoid\t       * (*lua_touserdata) (lua_State *L, int idx);\n\tlua_State      * (*lua_tothread) (lua_State *L, int idx);\n\tconst void     * (*lua_topointer) (lua_State *L, int idx);\n\tvoid (*lua_arith) (lua_State *L, int op);\n\tint (*lua_rawequal) (lua_State *L, int idx1, int idx2);\n\tint (*lua_compare) (lua_State *L, int idx1, int idx2, int op);\n\tvoid (*lua_pushnil) (lua_State *L);\n\tvoid (*lua_pushnumber) (lua_State *L, lua_Number n);\n\tvoid (*lua_pushinteger) (lua_State *L, lua_Integer n);\n\tconst char * (*lua_pushlstring) (lua_State *L, const char *s, size_t len);\n\tconst char * (*lua_pushexternalstring) (lua_State *L,\n\t\tconst char *s, size_t len, lua_Alloc falloc, void *ud);\n\tconst char * (*lua_pushstring) (lua_State *L, const char *s);\n\tconst char * (*lua_pushvfstring) (lua_State *L, const char *fmt,\n                                                      va_list argp);\n\tvoid (*lua_pushcclosure) (lua_State *L, lua_CFunction fn, int n);\n\tvoid (*lua_pushboolean) (lua_State *L, int b);\n\tvoid (*lua_pushlightuserdata) (lua_State *L, void *p);\n\tint (*lua_pushthread) (lua_State *L);\n\tint (*lua_getglobal) (lua_State *L, const char *name);\n\tint (*lua_gettable) (lua_State *L, int idx);\n\tint (*lua_getfield) (lua_State *L, int idx, const char *k);\n\tint (*lua_geti) (lua_State *L, int idx, lua_Integer n);\n\tint (*lua_rawget) (lua_State *L, int idx);\n\tint (*lua_rawgeti) (lua_State *L, int idx, lua_Integer n);\n\tint (*lua_rawgetp) (lua_State *L, int idx, const void *p);\n\tvoid (*lua_createtable) (lua_State *L, int narr, int nrec);\n\tvoid * (*lua_newuserdatauv) (lua_State *L, size_t sz, int nuvalue);\n\tint (*lua_getmetatable) (lua_State *L, int objindex);\n\tint (*lua_getiuservalue) (lua_State *L, int idx, int n);\n\tvoid (*lua_setglobal) (lua_State *L, const char *name);\n\tvoid (*lua_settable) (lua_State *L, int idx);\n\tvoid (*lua_setfield) (lua_State *L, int idx, const char *k);\n\tvoid (*lua_seti) (lua_State *L, int idx, lua_Integer n);\n\tvoid (*lua_rawset) (lua_State *L, int idx);\n\tvoid (*lua_rawseti) (lua_State *L, int idx, lua_Integer n);\n\tvoid (*lua_rawsetp) (lua_State *L, int idx, const void *p);\n\tint (*lua_setmetatable) (lua_State *L, int objindex);\n\tint (*lua_setiuservalue) (lua_State *L, int idx, int n);\n\tvoid (*lua_callk) (lua_State *L, int nargs, int nresults,\n                           lua_KContext ctx, lua_KFunction k);\n\tint (*lua_pcallk) (lua_State *L, int nargs, int nresults, int errfunc,\n                            lua_KContext ctx, lua_KFunction k);\n\tint (*lua_load) (lua_State *L, lua_Reader reader, void *dt,\n                          const char *chunkname, const char *mode);\n\tint (*lua_dump) (lua_State *L, lua_Writer writer, void *data, int strip);\n\tint (*lua_yieldk) (lua_State *L, int nresults, lua_KContext ctx,\n                               lua_KFunction k);\n\tint (*lua_resume) (lua_State *L, lua_State *from, int narg,\n                               int *nres);\n\tint (*lua_status) (lua_State *L);\n\tint (*lua_isyieldable) (lua_State *L);\n\tvoid (*lua_setwarnf) (lua_State *L, lua_WarnFunction f, void *ud);\n\tvoid (*lua_warning) (lua_State *L, const char *msg, int tocont);\n\tint (*lua_error) (lua_State *L);\n\tint (*lua_next) (lua_State *L, int idx);\n\tvoid (*lua_concat) (lua_State *L, int n);\n\tvoid (*lua_len) (lua_State *L, int idx);\n\tunsigned (*lua_numbertocstring) (lua_State *L, int idx, char *buff);\n\tsize_t (*lua_stringtonumber) (lua_State *L, const char *s);\n\tlua_Alloc (*lua_getallocf) (lua_State *L, void **ud);\n\tvoid (*lua_setallocf) (lua_State *L, lua_Alloc f, void *ud);\n\tvoid (*lua_toclose) (lua_State *L, int idx);\n\tvoid (*lua_closeslot) (lua_State *L, int idx);\n\tint (*lua_getstack) (lua_State *L, int level, lua_Debug *ar);\n\tint (*lua_getinfo) (lua_State *L, const char *what, lua_Debug *ar);\n\tconst char * (*lua_getlocal) (lua_State *L, const lua_Debug *ar, int n);\n\tconst char * (*lua_setlocal) (lua_State *L, const lua_Debug *ar, int n);\n\tconst char * (*lua_getupvalue) (lua_State *L, int funcindex, int n);\n\tconst char * (*lua_setupvalue) (lua_State *L, int funcindex, int n);\n\tvoid * (*lua_upvalueid) (lua_State *L, int fidx, int n);\n\tvoid (*lua_upvaluejoin) (lua_State *L, int fidx1, int n1,\n                                               int fidx2, int n2);\n\tvoid (*lua_sethook) (lua_State *L, lua_Hook func, int mask, int count);\n\tlua_Hook (*lua_gethook) (lua_State *L);\n\tint (*lua_gethookmask) (lua_State *L);\n\tint (*lua_gethookcount) (lua_State *L);\n\tvoid (*luaL_checkversion_) (lua_State *L, lua_Number ver, size_t sz);\n\tint (*luaL_getmetafield) (lua_State *L, int obj, const char *e);\n\tint (*luaL_callmeta) (lua_State *L, int obj, const char *e);\n\tconst char * (*luaL_tolstring) (lua_State *L, int idx, size_t *len);\n\tint (*luaL_argerror) (lua_State *L, int arg, const char *extramsg);\n\tint (*luaL_typeerror) (lua_State *L, int arg, const char *tname);\n\tconst char * (*luaL_checklstring) (lua_State *L, int arg,\n                                                          size_t *l);\n\tconst char * (*luaL_optlstring) (lua_State *L, int arg,\n                                          const char *def, size_t *l);\n\tlua_Number (*luaL_checknumber) (lua_State *L, int arg);\n\tlua_Number (*luaL_optnumber) (lua_State *L, int arg, lua_Number def);\n\tlua_Integer (*luaL_checkinteger) (lua_State *L, int arg);\n\tlua_Integer (*luaL_optinteger) (lua_State *L, int arg,\n                                          lua_Integer def);\n\tvoid (*luaL_checkstack) (lua_State *L, int sz, const char *msg);\n\tvoid (*luaL_checktype) (lua_State *L, int arg, int t);\n\tvoid (*luaL_checkany) (lua_State *L, int arg);\n\tint (*luaL_newmetatable) (lua_State *L, const char *tname);\n\tvoid (*luaL_setmetatable) (lua_State *L, const char *tname);\n\tvoid * (*luaL_testudata) (lua_State *L, int ud, const char *tname);\n\tvoid * (*luaL_checkudata) (lua_State *L, int ud, const char *tname);\n\tvoid (*luaL_where) (lua_State *L, int lvl);\n\tint (*luaL_checkoption) (lua_State *L, int arg, const char *def,\n                                   const char *const lst[]);\n\tint (*luaL_fileresult) (lua_State *L, int stat, const char *fname);\n\tint (*luaL_execresult) (lua_State *L, int stat);\n\tvoid * (*luaL_alloc) (void *ud, void *ptr, size_t osize,\n                                                    size_t nsize);\n\tint (*luaL_ref) (lua_State *L, int t);\n\tvoid (*luaL_unref) (lua_State *L, int t, int ref);\n\tint (*luaL_loadfilex) (lua_State *L, const char *filename,\n                                               const char *mode);\n\tint (*luaL_loadbufferx) (lua_State *L, const char *buff, size_t sz,\n                                   const char *name, const char *mode);\n\tint (*luaL_loadstring) (lua_State *L, const char *s);\n\tlua_State * (*luaL_newstate) (void);\n\tunsigned (*luaL_makeseed) (lua_State *L);\n\tlua_Integer (*luaL_len) (lua_State *L, int idx);\n\tvoid (*luaL_addgsub) (luaL_Buffer *b, const char *s,\n                                     const char *p, const char *r);\n\tconst char * (*luaL_gsub) (lua_State *L, const char *s,\n                                    const char *p, const char *r);\n\tvoid (*luaL_setfuncs) (lua_State *L, const luaL_Reg *l, int nup);\n\tint (*luaL_getsubtable) (lua_State *L, int idx, const char *fname);\n\tvoid (*luaL_traceback) (lua_State *L, lua_State *L1,\n                                  const char *msg, int level);\n\tvoid (*luaL_requiref) (lua_State *L, const char *modname,\n                                 lua_CFunction openf, int glb);\n\tvoid (*luaL_buffinit) (lua_State *L, luaL_Buffer *B);\n\tchar * (*luaL_prepbuffsize) (luaL_Buffer *B, size_t sz);\n\tvoid (*luaL_addlstring) (luaL_Buffer *B, const char *s, size_t l);\n\tvoid (*luaL_addstring) (luaL_Buffer *B, const char *s);\n\tvoid (*luaL_addvalue) (luaL_Buffer *B);\n\tvoid (*luaL_pushresult) (luaL_Buffer *B);\n\tvoid (*luaL_pushresultsize) (luaL_Buffer *B, size_t sz);\n\tchar * (*luaL_buffinitsize) (lua_State *L, luaL_Buffer *B, size_t sz);\n\tint (*lua_gc) (lua_State *L, int what, ...);\n};\n\nstruct lua_api *\nextlua_api() {\n\tstatic struct lua_api api = {\n\t\tLUA_VERSION_NUM,\n\n\t\tlua_newstate,\n\t\tlua_close,\n\t\tlua_newthread,\n\t\tlua_closethread,\n\t\tlua_atpanic,\n\t\tlua_version,\n\t\tlua_absindex,\n\t\tlua_gettop,\n\t\tlua_settop,\n\t\tlua_pushvalue,\n\t\tlua_rotate,\n\t\tlua_copy,\n\t\tlua_checkstack,\n\t\tlua_xmove,\n\t\tlua_isnumber,\n\t\tlua_isstring,\n\t\tlua_iscfunction,\n\t\tlua_isinteger,\n\t\tlua_isuserdata,\n\t\tlua_type,\n\t\tlua_typename,\n\t\tlua_tonumberx,\n\t\tlua_tointegerx,\n\t\tlua_toboolean,\n\t\tlua_tolstring,\n\t\tlua_rawlen,\n\t\tlua_tocfunction,\n\t\tlua_touserdata,\n\t\tlua_tothread,\n\t\tlua_topointer,\n\t\tlua_arith,\n\t\tlua_rawequal,\n\t\tlua_compare,\n\t\tlua_pushnil,\n\t\tlua_pushnumber,\n\t\tlua_pushinteger,\n\t\tlua_pushlstring,\n\t\tlua_pushexternalstring,\n\t\tlua_pushstring,\n\t\tlua_pushvfstring,\n\t\tlua_pushcclosure,\n\t\tlua_pushboolean,\n\t\tlua_pushlightuserdata,\n\t\tlua_pushthread,\n\t\tlua_getglobal,\n\t\tlua_gettable,\n\t\tlua_getfield,\n\t\tlua_geti,\n\t\tlua_rawget,\n\t\tlua_rawgeti,\n\t\tlua_rawgetp,\n\t\tlua_createtable,\n\t\tlua_newuserdatauv,\n\t\tlua_getmetatable,\n\t\tlua_getiuservalue,\n\t\tlua_setglobal,\n\t\tlua_settable,\n\t\tlua_setfield,\n\t\tlua_seti,\n\t\tlua_rawset,\n\t\tlua_rawseti,\n\t\tlua_rawsetp,\n\t\tlua_setmetatable,\n\t\tlua_setiuservalue,\n\t\tlua_callk,\n\t\tlua_pcallk,\n\t\tlua_load,\n\t\tlua_dump,\n\t\tlua_yieldk,\n\t\tlua_resume,\n\t\tlua_status,\n\t\tlua_isyieldable,\n\t\tlua_setwarnf,\n\t\tlua_warning,\n\t\tlua_error,\n\t\tlua_next,\n\t\tlua_concat,\n\t\tlua_len,\n\t\tlua_numbertocstring,\n\t\tlua_stringtonumber,\n\t\tlua_getallocf,\n\t\tlua_setallocf,\n\t\tlua_toclose,\n\t\tlua_closeslot,\n\t\tlua_getstack,\n\t\tlua_getinfo,\n\t\tlua_getlocal,\n\t\tlua_setlocal,\n\t\tlua_getupvalue,\n\t\tlua_setupvalue,\n\t\tlua_upvalueid,\n\t\tlua_upvaluejoin,\n\t\tlua_sethook,\n\t\tlua_gethook,\n\t\tlua_gethookmask,\n\t\tlua_gethookcount,\n\t\tluaL_checkversion_,\n\t\tluaL_getmetafield,\n\t\tluaL_callmeta,\n\t\tluaL_tolstring,\n\t\tluaL_argerror,\n\t\tluaL_typeerror,\n\t\tluaL_checklstring,\n\t\tluaL_optlstring,\n\t\tluaL_checknumber,\n\t\tluaL_optnumber,\n\t\tluaL_checkinteger,\n\t\tluaL_optinteger,\n\t\tluaL_checkstack,\n\t\tluaL_checktype,\n\t\tluaL_checkany,\n\t\tluaL_newmetatable,\n\t\tluaL_setmetatable,\n\t\tluaL_testudata,\n\t\tluaL_checkudata,\n\t\tluaL_where,\n\t\tluaL_checkoption,\n\t\tluaL_fileresult,\n\t\tluaL_execresult,\n\t\tluaL_alloc,\n\t\tluaL_ref,\n\t\tluaL_unref,\n\t\tluaL_loadfilex,\n\t\tluaL_loadbufferx,\n\t\tluaL_loadstring,\n\t\tluaL_newstate,\n\t\tluaL_makeseed,\n\t\tluaL_len,\n\t\tluaL_addgsub,\n\t\tluaL_gsub,\n\t\tluaL_setfuncs,\n\t\tluaL_getsubtable,\n\t\tluaL_traceback,\n\t\tluaL_requiref,\n\t\tluaL_buffinit,\n\t\tluaL_prepbuffsize,\n\t\tluaL_addlstring,\n\t\tluaL_addstring,\n\t\tluaL_addvalue,\n\t\tluaL_pushresult,\n\t\tluaL_pushresultsize,\n\t\tluaL_buffinitsize,\n\t\tlua_gc,\n\t};\n\treturn &api;\n}\n"
  },
  {
    "path": "extlua/extlua_impl.temp.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n\nstruct lua_api {\n\tint version;\n\n$API_DECL$\n};\n\nstruct lua_api *\nextlua_api() {\n\tstatic struct lua_api api = {\n\t\tLUA_VERSION_NUM,\n\n$API_STRUCT$\n\t};\n\treturn &api;\n}\n"
  },
  {
    "path": "extlua/extlua_sample.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n#include <stdint.h>\n#include <stddef.h>\n\n#include \"sokol/sokol_gfx.h\"\n#include \"perspective_quad.glsl.h\"\n#include \"solunaapi.h\"\n\nLUA_API void luaapi_init(lua_State *L);\nvoid sokolapi_init(lua_State *L);\n\n#if defined(_WIN32)\n#define EXTLUA_EXPORT __declspec(dllexport)\n#else\n#define EXTLUA_EXPORT __attribute__((visibility(\"default\")))\n#endif\n\n#define PQUAD_CORNER_N 4\n#define PQUAD_INFO_CORNER_MASK 0x3u\n#define PQUAD_INFO_USE_SPRITE_RECT 0x4u\n#define PQUAD_EPSILON 0.000001f\n\nstruct color {\n\tunsigned char channel[4];\n};\n\nstruct pquad_payload {\n\tuint32_t info;\n\tfloat q;\n\tstruct color color;\n};\n\nstruct pquad_inst {\n\tfloat pos_h0[3];\n\tfloat pos_h1[3];\n\tfloat pos_h2[3];\n\tfloat uv_rect[4];\n\tfloat q[4];\n\tstruct color color;\n};\n\nstruct material_perspective_quad {\n\tsg_pipeline pip;\n\tsg_buffer inst;\n\tstruct soluna_render_bindings bind;\n\tint base;\n\tvs_params_t *uniform;\n\tstruct soluna_sprite_bank bank;\n\tvoid *tmp_ptr;\n\tsize_t tmp_size;\n};\n\nstruct sprite_rect_basis {\n\tfloat scale_x;\n\tfloat scale_y;\n\tfloat shear_x;\n\tfloat shear_y;\n\tfloat tx;\n\tfloat ty;\n};\n\nstatic int material_id = 0;\n\nstatic void *\nfree_material_stream(void *ud, void *ptr, size_t osize, size_t nsize) {\n\t(void)ud;\n\t(void)osize;\n\tif (nsize == 0) {\n\t\tsoluna_material_stream_free(ptr);\n\t}\n\treturn NULL;\n}\n\nstatic sg_pipeline\nmake_pipeline(sg_pipeline_desc *desc) {\n\tsg_shader shd = sg_make_shader(perspective_quad_shader_desc(sg_query_backend()));\n\tdesc->shader = shd;\n\tdesc->primitive_type = SG_PRIMITIVETYPE_TRIANGLE_STRIP;\n\tdesc->label = \"extlua-perspective-quad-pipeline\";\n\tdesc->layout.buffers[0].step_func = SG_VERTEXSTEP_PER_INSTANCE;\n\tdesc->colors[0].blend = (sg_blend_state) {\n\t\t.enabled = true,\n\t\t.src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA,\n\t\t.dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,\n\t\t.src_factor_alpha = SG_BLENDFACTOR_ONE,\n\t\t.dst_factor_alpha = SG_BLENDFACTOR_ZERO,\n\t};\n\treturn sg_make_pipeline(desc);\n}\n\nstatic void\nset_position_homography(const float pos[PQUAD_CORNER_N][2], struct pquad_inst *inst) {\n\tconst float x0 = pos[0][0], y0 = pos[0][1];\n\tconst float x1 = pos[1][0], y1 = pos[1][1];\n\tconst float x2 = pos[2][0], y2 = pos[2][1];\n\tconst float x3 = pos[3][0], y3 = pos[3][1];\n\tconst float sx = x0 - x1 + x3 - x2;\n\tconst float sy = y0 - y1 + y3 - y2;\n\tconst float dx1 = x1 - x3;\n\tconst float dx2 = x2 - x3;\n\tconst float dy1 = y1 - y3;\n\tconst float dy2 = y2 - y3;\n\tconst float det = dx1 * dy2 - dx2 * dy1;\n\tfloat m31 = 0.0f;\n\tfloat m32 = 0.0f;\n\tfloat abs_det = det < 0.0f ? -det : det;\n\tif (abs_det > PQUAD_EPSILON) {\n\t\tm31 = (sx * dy2 - sy * dx2) / det;\n\t\tm32 = (sy * dx1 - sx * dy1) / det;\n\t}\n\tconst float m11 = x1 - x0 + m31 * x1;\n\tconst float m12 = x2 - x0 + m32 * x2;\n\tconst float m13 = x0;\n\tconst float m21 = y1 - y0 + m31 * y1;\n\tconst float m22 = y2 - y0 + m32 * y2;\n\tconst float m23 = y0;\n\tinst->pos_h0[0] = m11;\n\tinst->pos_h0[1] = m21;\n\tinst->pos_h0[2] = m31;\n\tinst->pos_h1[0] = m12;\n\tinst->pos_h1[1] = m22;\n\tinst->pos_h1[2] = m32;\n\tinst->pos_h2[0] = m13;\n\tinst->pos_h2[1] = m23;\n\tinst->pos_h2[2] = 1.0f;\n}\n\nstatic inline soluna_material_error\nperspective_quad_count(int prim_n, int *out) {\n\tif (prim_n % PQUAD_CORNER_N != 0) {\n\t\treturn \"Invalid perspective quad primitive count\";\n\t}\n\t*out = prim_n / PQUAD_CORNER_N;\n\treturn NULL;\n}\n\nstatic inline void\ndecode_sprite_rect_basis(struct sprite_rect_basis *basis, uint32_t corner, const struct soluna_material_stream_data *item) {\n\tfloat x = item->x;\n\tfloat y = item->y;\n\tswitch (corner) {\n\tcase 0:\n\t\tbasis->scale_x = x;\n\t\tbasis->scale_y = y;\n\t\tbreak;\n\tcase 1:\n\t\tbasis->shear_x = x;\n\t\tbasis->shear_y = y;\n\t\tbreak;\n\tcase 2:\n\t\tbasis->tx = x;\n\t\tbasis->ty = y;\n\t\tbreak;\n\t}\n}\n\nstatic inline void\nbuild_quad_from_rect(const struct sprite_rect_basis *basis, const struct soluna_sprite_rect *rect, float pos[PQUAD_CORNER_N][2]) {\n\tfloat scale_x = basis->scale_x - basis->tx;\n\tfloat scale_y = basis->scale_y - basis->ty;\n\tfloat shear_x = basis->shear_x - basis->tx;\n\tfloat shear_y = basis->shear_y - basis->ty;\n\tint corner;\n\tfor (corner=0; corner<PQUAD_CORNER_N; corner++) {\n\t\tfloat x = (corner & 1) ? (rect->w - rect->ox) : -rect->ox;\n\t\tfloat y = (corner >> 1) ? (rect->h - rect->oy) : -rect->oy;\n\t\tpos[corner][0] = x * scale_x + y * shear_x + basis->tx;\n\t\tpos[corner][1] = x * shear_y + y * scale_y + basis->ty;\n\t}\n}\n\nstatic void\nsubmit(void *m_, struct soluna_material_stream_context ctx, int n) {\n\tstruct material_perspective_quad *m = (struct material_perspective_quad *)m_;\n\tstruct pquad_inst *tmp = (struct pquad_inst *)m->tmp_ptr;\n\tint out_n;\n\tsoluna_material_error err = perspective_quad_count(n, &out_n);\n\tif (err != NULL) {\n\t\tsoluna_material_stream_error(ctx, err);\n\t\treturn;\n\t}\n\tint i;\n\tfor (i=0; i<out_n; i++) {\n\t\tstruct pquad_inst *inst = &tmp[i];\n\t\tint base = i * PQUAD_CORNER_N;\n\t\tint j;\n\t\tint sprite = -1;\n\t\tuint32_t stream_flags = 0;\n\t\tstruct sprite_rect_basis sprite_rect_basis = { 0 };\n\t\tfloat pos[PQUAD_CORNER_N][2];\n\t\tfor (j=0; j<PQUAD_CORNER_N; j++) {\n\t\t\tstruct soluna_material_stream_data item;\n\t\t\tstruct pquad_payload payload;\n\t\t\tif (!soluna_material_stream_read(ctx, base + j, sizeof(payload), &payload, &item)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tuint32_t flags = payload.info & PQUAD_INFO_USE_SPRITE_RECT;\n\t\t\tif (j == 0) {\n\t\t\t\tsprite = item.sprite;\n\t\t\t\tstream_flags = flags;\n\t\t\t\tinst->color = payload.color;\n\t\t\t} else if (item.sprite != sprite || flags != stream_flags) {\n\t\t\t\tsoluna_material_stream_error(ctx, \"Invalid perspective quad stream\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tuint32_t corner = payload.info & PQUAD_INFO_CORNER_MASK;\n\t\t\tif (corner >= PQUAD_CORNER_N) {\n\t\t\t\tsoluna_material_stream_error(ctx, \"Invalid perspective quad corner\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (stream_flags & PQUAD_INFO_USE_SPRITE_RECT) {\n\t\t\t\tdecode_sprite_rect_basis(&sprite_rect_basis, corner, &item);\n\t\t\t} else {\n\t\t\t\tpos[corner][0] = item.x;\n\t\t\t\tpos[corner][1] = item.y;\n\t\t\t}\n\t\t\tfloat q = payload.q;\n\t\t\tif (q <= PQUAD_EPSILON) {\n\t\t\t\tq = PQUAD_EPSILON;\n\t\t\t}\n\t\t\tinst->q[corner] = q;\n\t\t}\n\t\tstruct soluna_sprite_rect sprite_rect;\n\t\tif (!soluna_material_sprite_rect(m->bank, sprite, &sprite_rect)) {\n\t\t\tsoluna_material_stream_error(ctx, \"Invalid perspective quad sprite\");\n\t\t\treturn;\n\t\t}\n\t\tif (stream_flags & PQUAD_INFO_USE_SPRITE_RECT) {\n\t\t\tbuild_quad_from_rect(&sprite_rect_basis, &sprite_rect, pos);\n\t\t}\n\t\tset_position_homography(pos, inst);\n\t\tinst->uv_rect[0] = sprite_rect.u;\n\t\tinst->uv_rect[1] = sprite_rect.v;\n\t\tinst->uv_rect[2] = sprite_rect.w;\n\t\tinst->uv_rect[3] = sprite_rect.h;\n\t}\n\tsg_append_buffer(m->inst, &(sg_range) { tmp, out_n * sizeof(tmp[0]) });\n}\n\nstatic int\nlmaterial_perspective_quad_submit(lua_State *L) {\n\tstruct material_perspective_quad *m = (struct material_perspective_quad *)luaL_checkudata(L, 1, \"EXTLUA_MATERIAL_PERSPECTIVE_QUAD\");\n\tint inst_batch_n = (int)(m->tmp_size / sizeof(struct pquad_inst));\n\tif (inst_batch_n < 1) {\n\t\treturn luaL_error(L, \"Perspective quad tmp buffer is too small\");\n\t}\n\tconst void *stream = lua_touserdata(L, 2);\n\tint prim_n = luaL_checkinteger(L, 3);\n\tsoluna_material_error err = soluna_material_submit(stream, prim_n, material_id, inst_batch_n * PQUAD_CORNER_N, m, submit);\n\tif (err != NULL) {\n\t\treturn luaL_error(L, \"%s\", err);\n\t}\n\treturn 0;\n}\n\nstatic int\nlmaterial_perspective_quad_draw(lua_State *L) {\n\tstruct material_perspective_quad *m = (struct material_perspective_quad *)luaL_checkudata(L, 1, \"EXTLUA_MATERIAL_PERSPECTIVE_QUAD\");\n\tint prim_n = luaL_checkinteger(L, 3);\n\tif (prim_n <= 0) {\n\t\treturn 0;\n\t}\n\tint quad_n;\n\tsoluna_material_error err = perspective_quad_count(prim_n, &quad_n);\n\tif (err != NULL) {\n\t\treturn luaL_error(L, \"%s\", err);\n\t}\n\tsg_apply_pipeline(m->pip);\n\tsg_apply_uniforms(UB_vs_params, &(sg_range) { m->uniform, sizeof(vs_params_t) });\n\tsg_bindings bindings = soluna_material_bindings(m->bind);\n\tbindings.vertex_buffer_offsets[0] += (size_t)m->base * sizeof(struct pquad_inst);\n\tsg_apply_bindings(&bindings);\n\tsg_draw(0, 4, quad_n);\n\tm->base += quad_n;\n\treturn 0;\n}\n\nstatic int\nlmaterial_perspective_quad_reset(lua_State *L) {\n\tstruct material_perspective_quad *m = (struct material_perspective_quad *)luaL_checkudata(L, 1, \"EXTLUA_MATERIAL_PERSPECTIVE_QUAD\");\n\tm->base = 0;\n\treturn 0;\n}\n\nstatic int\nlset_material_id(lua_State *L) {\n\tint id = luaL_checkinteger(L, 1);\n\tif (id <= 0) {\n\t\treturn luaL_error(L, \"Invalid perspective quad material id %d\", id);\n\t}\n\tmaterial_id = id;\n\treturn 0;\n}\n\nstatic void\ninit_pipeline(struct material_perspective_quad *p) {\n\tsg_pipeline_desc desc = {\n\t\t.layout.attrs = {\n\t\t\t[ATTR_perspective_quad_pos_h0].format = SG_VERTEXFORMAT_FLOAT3,\n\t\t\t[ATTR_perspective_quad_pos_h1].format = SG_VERTEXFORMAT_FLOAT3,\n\t\t\t[ATTR_perspective_quad_pos_h2].format = SG_VERTEXFORMAT_FLOAT3,\n\t\t\t[ATTR_perspective_quad_uv_rect].format = SG_VERTEXFORMAT_FLOAT4,\n\t\t\t[ATTR_perspective_quad_q].format = SG_VERTEXFORMAT_FLOAT4,\n\t\t\t[ATTR_perspective_quad_color].format = SG_VERTEXFORMAT_UBYTE4N,\n\t\t},\n\t};\n\tp->pip = make_pipeline(&desc);\n}\n\nstatic int\nlnew_material_perspective_quad(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TTABLE);\n\tstruct material_perspective_quad *m = (struct material_perspective_quad *)lua_newuserdatauv(L, sizeof(*m), 4);\n\tint material_index = lua_gettop(L);\n\tinit_pipeline(m);\n\tm->base = 0;\n\n\tif (lua_getfield(L, 1, \"inst_buffer\") != LUA_TUSERDATA) {\n\t\treturn luaL_error(L, \"Invalid key .inst_buffer\");\n\t}\n\tluaL_checkudata(L, -1, \"SOKOL_BUFFER\");\n\tlua_pushvalue(L, -1);\n\tlua_setiuservalue(L, material_index, 1);\n\tlua_pushlightuserdata(L, &m->inst);\n\tlua_call(L, 1, 0);\n\n\tif (lua_getfield(L, 1, \"bindings\") != LUA_TUSERDATA) {\n\t\treturn luaL_error(L, \"Invalid key .bindings\");\n\t}\n\tm->bind = (struct soluna_render_bindings) {\n\t\t.ctx = luaL_checkudata(L, -1, \"SOKOL_BINDINGS\"),\n\t};\n\tlua_pushvalue(L, -1);\n\tlua_setiuservalue(L, material_index, 2);\n\tlua_pop(L, 1);\n\n\tif (lua_getfield(L, 1, \"uniform\") != LUA_TUSERDATA) {\n\t\treturn luaL_error(L, \"Invalid key .uniform\");\n\t}\n\tm->uniform = (vs_params_t *)luaL_checkudata(L, -1, \"SOKOL_UNIFORM\");\n\tlua_pushvalue(L, -1);\n\tlua_setiuservalue(L, material_index, 3);\n\tlua_pop(L, 1);\n\n\tif (lua_getfield(L, 1, \"sprite_bank\") != LUA_TLIGHTUSERDATA) {\n\t\treturn luaL_error(L, \"Invalid key .sprite_bank\");\n\t}\n\tm->bank = (struct soluna_sprite_bank) {\n\t\t.ctx = lua_touserdata(L, -1),\n\t};\n\tlua_pop(L, 1);\n\n\tif (lua_getfield(L, 1, \"tmp_buffer\") != LUA_TUSERDATA) {\n\t\treturn luaL_error(L, \"Invalid key .tmp_buffer\");\n\t}\n\tif (lua_getmetatable(L, -1)) {\n\t\treturn luaL_error(L, \"Not an userdata without metatable\");\n\t}\n\tm->tmp_ptr = lua_touserdata(L, -1);\n\tm->tmp_size = lua_rawlen(L, -1);\n\tlua_setiuservalue(L, material_index, 4);\n\n\tif (luaL_newmetatable(L, \"EXTLUA_MATERIAL_PERSPECTIVE_QUAD\")) {\n\t\tluaL_Reg l[] = {\n\t\t\t{ \"__index\", NULL },\n\t\t\t{ \"reset\", lmaterial_perspective_quad_reset },\n\t\t\t{ \"submit\", lmaterial_perspective_quad_submit },\n\t\t\t{ \"draw\", lmaterial_perspective_quad_draw },\n\t\t\t{ NULL, NULL },\n\t\t};\n\t\tluaL_setfuncs(L, l, 0);\n\t\tlua_pushvalue(L, -1);\n\t\tlua_setfield(L, -2, \"__index\");\n\t}\n\tlua_setmetatable(L, -2);\n\treturn 1;\n}\n\nstatic inline float\nget_number_field(lua_State *L, int index, const char *field, float defv) {\n\tfloat v;\n\tlua_getfield(L, index, field);\n\tv = luaL_optnumber(L, -1, defv);\n\tlua_pop(L, 1);\n\treturn v;\n}\n\nstatic int\nget_quad(lua_State *L, int index, float quad[8]) {\n\tif (lua_getfield(L, index, \"quad\") == LUA_TTABLE) {\n\t\tint i;\n\t\tfor (i=0; i<8; i++) {\n\t\t\tlua_geti(L, -1, i + 1);\n\t\t\tquad[i] = luaL_checknumber(L, -1);\n\t\t\tlua_pop(L, 1);\n\t\t}\n\t\tlua_pop(L, 1);\n\t\treturn 0;\n\t}\n\tlua_pop(L, 1);\n\treturn 1;\n}\n\nstatic void\nget_q(lua_State *L, int index, float q[4]) {\n\tint i;\n\tif (lua_getfield(L, index, \"q\") != LUA_TTABLE) {\n\t\tfor (i=0; i<4; i++) {\n\t\t\tq[i] = 1.0f;\n\t\t}\n\t\tlua_pop(L, 1);\n\t\treturn;\n\t}\n\tfor (i=0; i<4; i++) {\n\t\tlua_geti(L, -1, i + 1);\n\t\tq[i] = luaL_optnumber(L, -1, 1.0f);\n\t\tlua_pop(L, 1);\n\t}\n\tlua_pop(L, 1);\n}\n\nstatic struct color\nget_color(lua_State *L, int index) {\n\tuint32_t color;\n\tstruct color c;\n\tlua_getfield(L, index, \"color\");\n\tcolor = (uint32_t)luaL_optinteger(L, -1, 0xffffffff);\n\tif (!(color & 0xff000000)) {\n\t\tcolor |= 0xff000000;\n\t}\n\tc.channel[0] = (color >> 16) & 0xff;\n\tc.channel[1] = (color >> 8) & 0xff;\n\tc.channel[2] = color & 0xff;\n\tc.channel[3] = (color >> 24) & 0xff;\n\tlua_pop(L, 1);\n\treturn c;\n}\n\nstruct pquad_stream_context {\n\tint sprite;\n\tint use_sprite_rect;\n\tfloat scale_x;\n\tfloat scale_y;\n\tfloat shear_x;\n\tfloat shear_y;\n\tfloat quad[8];\n\tfloat q[4];\n\tstruct color color;\n\tstruct pquad_payload payload[PQUAD_CORNER_N];\n};\n\nstatic void\nwrite_perspective_quad_stream(void *ud, int index, struct soluna_material_stream_item *item) {\n\tstruct pquad_stream_context *ctx = (struct pquad_stream_context *)ud;\n\titem->sprite = ctx->sprite;\n\tif (ctx->use_sprite_rect) {\n\t\tswitch (index) {\n\t\tcase 0:\n\t\t\titem->x = ctx->scale_x;\n\t\t\titem->y = ctx->scale_y;\n\t\t\tbreak;\n\t\tcase 1:\n\t\t\titem->x = ctx->shear_x;\n\t\t\titem->y = ctx->shear_y;\n\t\t\tbreak;\n\t\tdefault:\n\t\t\titem->x = 0.0f;\n\t\t\titem->y = 0.0f;\n\t\t\tbreak;\n\t\t}\n\t} else {\n\t\tfloat x = ctx->quad[index * 2];\n\t\tfloat y = ctx->quad[index * 2 + 1];\n\t\titem->x = x * ctx->scale_x + y * ctx->shear_x;\n\t\titem->y = x * ctx->shear_y + y * ctx->scale_y;\n\t}\n\tstruct pquad_payload *payload = &ctx->payload[index];\n\tuint32_t info = (uint32_t)index;\n\tif (ctx->use_sprite_rect) {\n\t\tinfo |= PQUAD_INFO_USE_SPRITE_RECT;\n\t}\n\tpayload->info = info;\n\tpayload->q = ctx->q[index];\n\tpayload->color = ctx->color;\n\titem->payload = payload;\n}\n\nstatic int\nlperspective_quad_sprite(lua_State *L) {\n\tif (material_id <= 0) {\n\t\treturn luaL_error(L, \"Perspective quad material is not registered\");\n\t}\n\tluaL_checktype(L, 2, LUA_TTABLE);\n\tstruct pquad_stream_context ctx;\n\tctx.sprite = luaL_checkinteger(L, 1) - 1;\n\tctx.use_sprite_rect = get_quad(L, 2, ctx.quad);\n\tctx.scale_x = get_number_field(L, 2, \"scale_x\", 1.0f);\n\tctx.scale_y = get_number_field(L, 2, \"scale_y\", 1.0f);\n\tctx.shear_x = get_number_field(L, 2, \"shear_x\", 0.0f);\n\tctx.shear_y = get_number_field(L, 2, \"shear_y\", 0.0f);\n\tget_q(L, 2, ctx.q);\n\tctx.color = get_color(L, 2);\n\tstruct soluna_material_stream stream;\n\tsoluna_material_error err = soluna_material_push_stream(material_id, PQUAD_CORNER_N, sizeof(struct pquad_payload), write_perspective_quad_stream, &ctx, &stream);\n\tif (err != NULL) {\n\t\treturn luaL_error(L, \"%s\", err);\n\t}\n\tlua_pushexternalstring(L, stream.data, stream.size, free_material_stream, NULL);\n\treturn 1;\n}\n\nstatic int\nluaopen_ext_material_perspective_quad(lua_State *L) {\n\tluaL_checkversion(L);\n\tluaL_Reg l[] = {\n\t\t{ \"set_material_id\", lset_material_id },\n\t\t{ \"new\", lnew_material_perspective_quad },\n\t\t{ \"sprite\", lperspective_quad_sprite },\n\t\t{ \"instance_size\", NULL },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\tlua_pushinteger(L, sizeof(struct pquad_inst));\n\tlua_setfield(L, -2, \"instance_size\");\n\treturn 1;\n}\n\nstatic int\nlhello(lua_State *L) {\n\tlua_pushstring(L, \"Hello World From Sample\");\n\treturn 1;\n}\n\nstatic int\nluaopen_foobar(lua_State *L) {\n\tluaL_Reg l[] = {\n\t\t{ \"hello\", lhello },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\treturn 1;\n}\n\nEXTLUA_EXPORT int\nextlua_init(lua_State *L) {\n\tluaapi_init(L);\n\tsokolapi_init(L);\n\tsolunaapi_init(L);\n\tluaL_Reg l[] = {\n\t\t{ \"ext.foobar\", luaopen_foobar },\n\t\t{ \"ext.material.perspective_quad\", luaopen_ext_material_perspective_quad },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\treturn 1;\n}\n"
  },
  {
    "path": "extlua/gen.lua",
    "content": "-- extlua.h[/.c] generator\n\nlocal luapath = \"../3rd/lua/\"\n\nlocal function parse_params(params)\n\tlocal p = {}\n\tif params == \"(void)\" then\n\t\treturn p\n\tend\n\tlocal pn = 1\n\tfor t, n in params:gmatch (\"(.-)([%w_.%[%]]+)[,)]\", 2) do\n\t\tlocal is_array\n\t\tif n:sub(-2) == \"[]\" then\n\t\t\tis_array = true\n\t\t\tn = n:sub(1, -3)\n\t\tend\n\t\tp[pn] = { type = t, name = n, is_array = is_array }\n\t\tpn = pn + 1\n\tend\n\treturn p\nend\n\nlocal function get_apis(result, filename, prefix)\n\tlocal f = assert(io.open(luapath .. filename))\n\tlocal src = f:read \"*a\"\n\tf:close()\n\tlocal n = #result + 1\n\tfor line in src:gmatch( prefix .. \"%s*(.-;)%s*\\n\" ) do\n\t\tif not line:find(\"...\", 1, true) then\n\t\t\tlocal retv, funcname, params = line:match \"([%s%w_*]+)%(([%s%w_]+)%)%s*(%b());$\"\n\t\t\tif retv == nil then\n\t\t\t\tif line:match \"%S\" then\n\t\t\t\t\terror(\"Invalid function \" .. line)\n\t\t\t\tend\n\t\t\tend\n\t\t\tretv = retv:gsub(\"%s+$\", \"\")\n\t\t\tif retv:match \"%s*void%s*$\" then\n\t\t\t\tretv = \"void\"\n\t\t\tend\n\t\t\tresult[n] = {\n\t\t\t\tret = retv,\n\t\t\t\tname = funcname,\n\t\t\t\tparams = parse_params(params),\n\t\t\t}\n\t\t\tn = n + 1\n\t\tend\n\tend\nend\n\nlocal function decls(params)\n\tif #params == 0 then\n\t\treturn \"void\"\n\tend\n\tlocal r = {}\n\tfor idx, p in ipairs(params) do\n\t\tlocal d = p.type .. p.name\n\t\tif p.is_array then\n\t\t\td = d .. \"[]\"\n\t\tend\n\t\tr[idx] = d\n\tend\n\treturn table.concat(r, \",\")\nend\n\nlocal function gen_decl(apis)\n\tlocal d = {}\n\tfor idx, item in ipairs(apis) do\n\t\td[idx] = (\"\\t%s (*%s) (%s);\"):format(item.ret, item.name, decls(item.params))\n\tend\n\td[#d+1] = \"\\tint (*lua_gc) (lua_State *L, int what, ...);\"\n\treturn table.concat(d, \"\\n\")\nend\n\nlocal function gen_struct(apis)\n\tlocal d = {}\n\tfor idx, item in ipairs(apis) do\n\t\td[idx] = (\"\\t\\t%s,\"):format(item.name)\n\tend\n\td[#d+1] = \"\\t\\tlua_gc,\"\n\treturn table.concat(d, \"\\n\")\nend\n\nlocal function args(params)\n\tlocal args = {}\n\tfor idx, p in ipairs(params) do\n\t\targs[idx] = p.name\n\tend\n\treturn table.concat(args, \",\")\nend\n\nlocal impl_fmt = [[\nLUA_API %s\n%s(%s) {\n\t%sAPI.%s(%s);\n}\n\n]]\nlocal function gen_impl(apis)\n\tlocal d = {}\n\tlocal n = 1\n\tfor idx, item in ipairs(apis) do\n\t\tif item.name ~= \"lua_gc\" then\n\t\t\tlocal ret\n\t\t\tif item.ret == \"void\" then\n\t\t\t\tret = \"\"\n\t\t\telse\n\t\t\t\tret = \"return \"\n\t\t\tend\n\t\t\tlocal impl = impl_fmt:format(item.ret, item.name, decls(item.params),\n\t\t\t\tret, item.name, args(item.params))\n\t\t\td[n] = impl; n = n + 1\n\t\tend\n\tend\n\treturn table.concat(d)\nend\n\nlocal function readfile(filename)\n\tlocal f = assert(io.open(filename))\n\tlocal content = f:read \"a\"\n\tf:close()\n\treturn content\nend\n\nlocal function genfile(filename, temp)\n\tlocal t = readfile(filename)\n\tlocal output_filename = filename:gsub(\"%.temp\", \"\")\n\tlocal output_f = assert(io.open(output_filename, \"w\"))\n\toutput_f:write(\"// AUTO GENERATED by \" .. filename  .. \", DONT EDIT\\n\\n\")\n\toutput_f:write((t:gsub(\"%$([%w_]+)%$\", temp)))\n\toutput_f:close()\nend\n\nlocal apis = {}\nget_apis(apis, \"lua.h\", \"LUA_API\")\nget_apis(apis, \"lauxlib.h\", \"LUALIB_API\")\n\nlocal convert = {\n\tAPI_DECL = gen_decl(apis),\n\tAPI_STRUCT = gen_struct(apis),\n\tAPI_IMPL = gen_impl(apis),\n}\n\ngenfile(\"extlua.temp.c\", convert)\ngenfile(\"extlua_impl.temp.c\", convert)\n"
  },
  {
    "path": "extlua/gen_sokol.lua",
    "content": "-- sokolapi.c generator\n\nlocal header = \"../3rd/sokol/sokol_gfx.h\"\n\nlocal allowlist = {\n\tsg_make_buffer = true,\n\tsg_destroy_buffer = true,\n\tsg_update_buffer = true,\n\tsg_append_buffer = true,\n\tsg_make_shader = true,\n\tsg_destroy_shader = true,\n\tsg_make_pipeline = true,\n\tsg_destroy_pipeline = true,\n\tsg_apply_pipeline = true,\n\tsg_apply_bindings = true,\n\tsg_apply_uniforms = true,\n\tsg_draw = true,\n\tsg_draw_ex = true,\n\tsg_query_backend = true,\n\tsg_query_buffer_state = true,\n\tsg_query_shader_state = true,\n\tsg_query_pipeline_state = true,\n}\n\nlocal function parse_params(params)\n\tlocal p = {}\n\tif params == \"(void)\" then\n\t\treturn p\n\tend\n\tlocal body = params:sub(2, -2)\n\tlocal n = 1\n\tfor decl in body:gmatch \"[^,]+\" do\n\t\tlocal t, name = decl:match \"^(.-)([%w_]+)%s*$\"\n\t\tif not t then\n\t\t\terror(\"Invalid param \" .. decl)\n\t\tend\n\t\tp[n] = { type = t, name = name }\n\t\tn = n + 1\n\tend\n\treturn p\nend\n\nlocal function readfile(filename)\n\tlocal f = assert(io.open(filename))\n\tlocal content = f:read \"a\"\n\tf:close()\n\treturn content\nend\n\nlocal function get_apis()\n\tlocal src = readfile(header)\n\tlocal apis = {}\n\tfor line in src:gmatch \"SOKOL_GFX_API_DECL%s*(.-;)%s*\\n\" do\n\t\tlocal retv, name, params = line:match \"([%s%w_*]+)%s+(sg_[%w_]+)%s*(%b());$\"\n\t\tif name and allowlist[name] then\n\t\t\tapis[#apis + 1] = {\n\t\t\t\tret = retv:gsub(\"%s+$\", \"\"),\n\t\t\t\tname = name,\n\t\t\t\tparams = parse_params(params),\n\t\t\t}\n\t\tend\n\tend\n\treturn apis\nend\n\nlocal function decls(params)\n\tif #params == 0 then\n\t\treturn \"void\"\n\tend\n\tlocal r = {}\n\tfor i, p in ipairs(params) do\n\t\tr[i] = p.type .. p.name\n\tend\n\treturn table.concat(r, \", \")\nend\n\nlocal function args(params)\n\tlocal r = {}\n\tfor i, p in ipairs(params) do\n\t\tr[i] = p.name\n\tend\n\treturn table.concat(r, \", \")\nend\n\nlocal function gen_decl(apis)\n\tlocal r = {}\n\tfor i, api in ipairs(apis) do\n\t\tr[i] = (\"\\t%s (*%s) (%s);\"):format(api.ret, api.name, decls(api.params))\n\tend\n\treturn table.concat(r, \"\\n\")\nend\n\nlocal function gen_struct(apis)\n\tlocal r = {}\n\tfor i, api in ipairs(apis) do\n\t\tr[i] = (\"\\t\\t%s,\"):format(api.name)\n\tend\n\treturn table.concat(r, \"\\n\")\nend\n\nlocal function gen_impl(apis)\n\tlocal r = {}\n\tfor i, api in ipairs(apis) do\n\t\tlocal ret = api.ret == \"void\" and \"\" or \"return \"\n\t\tr[i] = (\"SOKOL_GFX_API_DECL %s\\n%s(%s) {\\n\\t%sAPI.%s(%s);\\n}\\n\\n\"):format(\n\t\t\tapi.ret,\n\t\t\tapi.name,\n\t\t\tdecls(api.params),\n\t\t\tret,\n\t\t\tapi.name,\n\t\t\targs(api.params)\n\t\t)\n\tend\n\treturn table.concat(r)\nend\n\nlocal function genfile(filename, temp)\n\tlocal t = readfile(filename)\n\tlocal output = filename:gsub(\"%.temp\", \"\")\n\tlocal f = assert(io.open(output, \"w\"))\n\tf:write(\"// AUTO GENERATED by \" .. filename .. \", DONT EDIT\\n\\n\")\n\tf:write((t:gsub(\"%$([%w_]+)%$\", temp)))\n\tf:close()\nend\n\nlocal apis = get_apis()\nlocal convert = {\n\tAPI_DECL = gen_decl(apis),\n\tAPI_STRUCT = gen_struct(apis),\n\tAPI_IMPL = gen_impl(apis),\n}\n\ngenfile(\"sokolapi.temp.c\", convert)\ngenfile(\"sokolapi_impl.temp.c\", convert)\n"
  },
  {
    "path": "extlua/gen_soluna.lua",
    "content": "-- solunaapi.c generator\n\nlocal apis = {\n\t{\n\t\tret = \"soluna_material_error\",\n\t\tname = \"soluna_material_submit\",\n\t\tparams = {\n\t\t\t{ type = \"const void *\", name = \"stream\" },\n\t\t\t{ type = \"int \", name = \"prim_n\" },\n\t\t\t{ type = \"int \", name = \"material_id\" },\n\t\t\t{ type = \"int \", name = \"batch_n\" },\n\t\t\t{ type = \"void *\", name = \"ud\" },\n\t\t\t{ type = \"soluna_material_submit_func \", name = \"submit\" },\n\t\t},\n\t},\n\t{\n\t\tret = \"int\",\n\t\tname = \"soluna_material_sprite_rect\",\n\t\tparams = {\n\t\t\t{ type = \"struct soluna_sprite_bank \", name = \"bank\" },\n\t\t\t{ type = \"int \", name = \"sprite\" },\n\t\t\t{ type = \"struct soluna_sprite_rect *\", name = \"out\" },\n\t\t},\n\t},\n\t{\n\t\tret = \"sg_bindings\",\n\t\tname = \"soluna_material_bindings\",\n\t\tparams = {\n\t\t\t{ type = \"struct soluna_render_bindings \", name = \"bindings\" },\n\t\t},\n\t},\n\t{\n\t\tret = \"soluna_material_error\",\n\t\tname = \"soluna_material_push_stream\",\n\t\tparams = {\n\t\t\t{ type = \"int \", name = \"material_id\" },\n\t\t\t{ type = \"int \", name = \"count\" },\n\t\t\t{ type = \"size_t \", name = \"payload_size\" },\n\t\t\t{ type = \"soluna_material_stream_write_func \", name = \"write\" },\n\t\t\t{ type = \"void *\", name = \"ud\" },\n\t\t\t{ type = \"struct soluna_material_stream *\", name = \"out\" },\n\t\t},\n\t},\n\t{\n\t\tret = \"void\",\n\t\tname = \"soluna_material_stream_free\",\n\t\tparams = {\n\t\t\t{ type = \"void *\", name = \"ptr\" },\n\t\t},\n\t},\n\t{\n\t\tret = \"int\",\n\t\tname = \"soluna_material_stream_read\",\n\t\tparams = {\n\t\t\t{ type = \"struct soluna_material_stream_context \", name = \"ctx\" },\n\t\t\t{ type = \"int \", name = \"index\" },\n\t\t\t{ type = \"size_t \", name = \"payload_size\" },\n\t\t\t{ type = \"void *\", name = \"payload\" },\n\t\t\t{ type = \"struct soluna_material_stream_data *\", name = \"out\" },\n\t\t},\n\t},\n\t{\n\t\tret = \"void\",\n\t\tname = \"soluna_material_stream_error\",\n\t\tparams = {\n\t\t\t{ type = \"struct soluna_material_stream_context \", name = \"ctx\" },\n\t\t\t{ type = \"const char *\", name = \"error\" },\n\t\t},\n\t},\n\t{\n\t\tret = \"int\",\n\t\tname = \"soluna_material_stream_failed\",\n\t\tparams = {\n\t\t\t{ type = \"struct soluna_material_stream_context \", name = \"ctx\" },\n\t\t},\n\t},\n}\n\nlocal type_decl = [[\n#define SOLUNA_EXT_API_VERSION 1\n\nstruct soluna_sprite_rect {\n\tint texture;\n\tfloat u;\n\tfloat v;\n\tfloat w;\n\tfloat h;\n\tfloat ox;\n\tfloat oy;\n};\n\nstruct soluna_material_stream_item {\n\tfloat x;\n\tfloat y;\n\tint sprite;\n\tconst void *payload;\n};\n\nstruct soluna_material_stream_data {\n\tfloat x;\n\tfloat y;\n\tint sprite;\n};\n\nstruct soluna_material_stream {\n\tchar *data;\n\tsize_t size;\n};\n\ntypedef const char *soluna_material_error;\n\nstruct soluna_material_stream_context {\n\tvoid *ctx;\n};\n\nstruct soluna_render_bindings {\n\tvoid *ctx;\n};\n\nstruct soluna_sprite_bank {\n\tvoid *ctx;\n};\n\ntypedef void (*soluna_material_submit_func)(void *ud, struct soluna_material_stream_context ctx, int n);\ntypedef void (*soluna_material_stream_write_func)(void *ud, int index, struct soluna_material_stream_item *item);\n]]\n\nlocal host_type_decl = [[\n#include <stddef.h>\n\n#include \"sokol/sokol_gfx.h\"\n\n]] .. type_decl\n\nlocal function readfile(filename)\n\tlocal f = assert(io.open(filename))\n\tlocal content = f:read \"a\"\n\tf:close()\n\treturn content\nend\n\nlocal function genfile(filename, temp)\n\tlocal t = readfile(filename)\n\tlocal output = filename:gsub(\"%.temp\", \"\")\n\tlocal f = assert(io.open(output, \"w\"))\n\tf:write(\"// AUTO GENERATED by \" .. filename .. \", DONT EDIT\\n\\n\")\n\tf:write((t:gsub(\"%$([%w_]+)%$\", temp)))\n\tf:close()\nend\n\nlocal function decls(params)\n\tif #params == 0 then\n\t\treturn \"void\"\n\tend\n\tlocal r = {}\n\tfor i, p in ipairs(params) do\n\t\tr[i] = p.type .. p.name\n\tend\n\treturn table.concat(r, \", \")\nend\n\nlocal function args(params)\n\tlocal r = {}\n\tfor i, p in ipairs(params) do\n\t\tr[i] = p.name\n\tend\n\treturn table.concat(r, \", \")\nend\n\nlocal function field_name(api)\n\treturn api.field or api.name:gsub(\"^soluna_\", \"\")\nend\n\nlocal function impl_name(api)\n\treturn api.impl or field_name(api)\nend\n\nlocal function gen_header_decl()\n\tlocal r = { \"void solunaapi_init(lua_State *L);\" }\n\tfor _, api in ipairs(apis) do\n\t\tr[#r + 1] = (\"%s %s(%s);\"):format(api.ret, api.name, decls(api.params))\n\tend\n\treturn table.concat(r, \"\\n\")\nend\n\nlocal function gen_api_decl()\n\tlocal r = {}\n\tfor i, api in ipairs(apis) do\n\t\tr[i] = (\"\\t%s (*%s) (%s);\"):format(api.ret, field_name(api), decls(api.params))\n\tend\n\treturn table.concat(r, \"\\n\")\nend\n\nlocal function gen_api_struct()\n\tlocal r = {}\n\tfor i, api in ipairs(apis) do\n\t\tr[i] = (\"\\t\\t%s,\"):format(impl_name(api))\n\tend\n\treturn table.concat(r, \"\\n\")\nend\n\nlocal function gen_api_extern()\n\tlocal r = {}\n\tfor i, api in ipairs(apis) do\n\t\tr[i] = (\"extern %s %s(%s);\"):format(api.ret, impl_name(api), decls(api.params))\n\tend\n\treturn table.concat(r, \"\\n\")\nend\n\nlocal function gen_api_impl()\n\tlocal r = {}\n\tfor i, api in ipairs(apis) do\n\t\tlocal ret = api.ret == \"void\" and \"\" or \"return \"\n\t\tr[i] = (\"%s\\n%s(%s) {\\n\\t%sAPI.%s(%s);\\n}\\n\\n\"):format(\n\t\t\tapi.ret,\n\t\t\tapi.name,\n\t\t\tdecls(api.params),\n\t\t\tret,\n\t\t\tfield_name(api),\n\t\t\targs(api.params)\n\t\t)\n\tend\n\treturn table.concat(r)\nend\n\nlocal convert = {\n\tTYPE_DECL = type_decl,\n\tHEADER_DECL = gen_header_decl(),\n\tAPI_DECL = gen_api_decl(),\n\tAPI_STRUCT = gen_api_struct(),\n\tAPI_EXTERN = gen_api_extern(),\n\tAPI_IMPL = gen_api_impl(),\n\tHOST_TYPE_DECL = host_type_decl,\n}\n\ngenfile(\"solunaapi.h.temp\", convert)\ngenfile(\"solunaapi.temp.c\", convert)\ngenfile(\"solunaapi_impl.temp.c\", convert)\ngenfile(\"../src/extapi_types.temp.h\", convert)\n"
  },
  {
    "path": "extlua/perspective_quad.glsl",
    "content": "@vs vs\nlayout(binding=0) uniform vs_params {\n\tvec2 framesize;\n\tfloat texsize;\n};\n\nin vec3 pos_h0;\nin vec3 pos_h1;\nin vec3 pos_h2;\nin vec4 uv_rect;\nin vec4 q;\n\nin vec4 color;\n\nout vec3 uvq;\nout vec4 frag_color;\nout flat float tex_scale;\n\nvoid main() {\n\tvec2 corner = vec2(float(gl_VertexIndex & 1), float(gl_VertexIndex >> 1));\n\tmat3 pos_h = mat3(pos_h0, pos_h1, pos_h2);\n\tvec3 pos_hv = pos_h * vec3(corner, 1.0);\n\tfloat pos_w = max(pos_hv.z, 1e-6);\n\tvec2 pos = pos_hv.xy / pos_w;\n\tvec2 uv = uv_rect.xy + uv_rect.zw * corner;\n\tfloat qx0 = mix(q.x, q.y, corner.x);\n\tfloat qx1 = mix(q.z, q.w, corner.x);\n\tfloat qv = max(mix(qx0, qx1, corner.y), 1e-6);\n\tuvq = vec3(uv * qv, qv);\n\tvec2 clip = pos * framesize;\n\tgl_Position = vec4(clip.x - 1.0, clip.y + 1.0, 0.0, 1.0);\n\tfrag_color = color;\n\ttex_scale = texsize;\n}\n\n@end\n\n@fs fs\nlayout(binding=1) uniform texture2D tex;\nlayout(binding=0) uniform sampler smp;\n\nin vec3 uvq;\nin vec4 frag_color;\nin flat float tex_scale;\nout vec4 out_color;\n\nvoid main() {\n\tvec3 proj_uv = vec3(uvq.xy * tex_scale, uvq.z);\n\tout_color = textureProj(sampler2D(tex, smp), proj_uv) * frag_color;\n}\n@end\n\n@program perspective_quad vs fs\n"
  },
  {
    "path": "extlua/sokolapi.c",
    "content": "// AUTO GENERATED by sokolapi.temp.c, DONT EDIT\n\n#include <lua.h>\n\n#include \"sokol/sokol_gfx.h\"\n\nstruct sokol_api {\n\tint version;\n\n\tsg_buffer (*sg_make_buffer) (const sg_buffer_desc* desc);\n\tsg_shader (*sg_make_shader) (const sg_shader_desc* desc);\n\tsg_pipeline (*sg_make_pipeline) (const sg_pipeline_desc* desc);\n\tvoid (*sg_destroy_buffer) (sg_buffer buf);\n\tvoid (*sg_destroy_shader) (sg_shader shd);\n\tvoid (*sg_destroy_pipeline) (sg_pipeline pip);\n\tvoid (*sg_update_buffer) (sg_buffer buf,  const sg_range* data);\n\tint (*sg_append_buffer) (sg_buffer buf,  const sg_range* data);\n\tvoid (*sg_apply_pipeline) (sg_pipeline pip);\n\tvoid (*sg_apply_bindings) (const sg_bindings* bindings);\n\tvoid (*sg_apply_uniforms) (int ub_slot,  const sg_range* data);\n\tvoid (*sg_draw) (int base_element,  int num_elements,  int num_instances);\n\tvoid (*sg_draw_ex) (int base_element,  int num_elements,  int num_instances,  int base_vertex,  int base_instance);\n\tsg_backend (*sg_query_backend) (void);\n\tsg_resource_state (*sg_query_buffer_state) (sg_buffer buf);\n\tsg_resource_state (*sg_query_shader_state) (sg_shader shd);\n\tsg_resource_state (*sg_query_pipeline_state) (sg_pipeline pip);\n};\n\nstatic struct sokol_api API;\n\nSOKOL_GFX_API_DECL sg_buffer\nsg_make_buffer(const sg_buffer_desc* desc) {\n\treturn API.sg_make_buffer(desc);\n}\n\nSOKOL_GFX_API_DECL sg_shader\nsg_make_shader(const sg_shader_desc* desc) {\n\treturn API.sg_make_shader(desc);\n}\n\nSOKOL_GFX_API_DECL sg_pipeline\nsg_make_pipeline(const sg_pipeline_desc* desc) {\n\treturn API.sg_make_pipeline(desc);\n}\n\nSOKOL_GFX_API_DECL void\nsg_destroy_buffer(sg_buffer buf) {\n\tAPI.sg_destroy_buffer(buf);\n}\n\nSOKOL_GFX_API_DECL void\nsg_destroy_shader(sg_shader shd) {\n\tAPI.sg_destroy_shader(shd);\n}\n\nSOKOL_GFX_API_DECL void\nsg_destroy_pipeline(sg_pipeline pip) {\n\tAPI.sg_destroy_pipeline(pip);\n}\n\nSOKOL_GFX_API_DECL void\nsg_update_buffer(sg_buffer buf,  const sg_range* data) {\n\tAPI.sg_update_buffer(buf, data);\n}\n\nSOKOL_GFX_API_DECL int\nsg_append_buffer(sg_buffer buf,  const sg_range* data) {\n\treturn API.sg_append_buffer(buf, data);\n}\n\nSOKOL_GFX_API_DECL void\nsg_apply_pipeline(sg_pipeline pip) {\n\tAPI.sg_apply_pipeline(pip);\n}\n\nSOKOL_GFX_API_DECL void\nsg_apply_bindings(const sg_bindings* bindings) {\n\tAPI.sg_apply_bindings(bindings);\n}\n\nSOKOL_GFX_API_DECL void\nsg_apply_uniforms(int ub_slot,  const sg_range* data) {\n\tAPI.sg_apply_uniforms(ub_slot, data);\n}\n\nSOKOL_GFX_API_DECL void\nsg_draw(int base_element,  int num_elements,  int num_instances) {\n\tAPI.sg_draw(base_element, num_elements, num_instances);\n}\n\nSOKOL_GFX_API_DECL void\nsg_draw_ex(int base_element,  int num_elements,  int num_instances,  int base_vertex,  int base_instance) {\n\tAPI.sg_draw_ex(base_element, num_elements, num_instances, base_vertex, base_instance);\n}\n\nSOKOL_GFX_API_DECL sg_backend\nsg_query_backend(void) {\n\treturn API.sg_query_backend();\n}\n\nSOKOL_GFX_API_DECL sg_resource_state\nsg_query_buffer_state(sg_buffer buf) {\n\treturn API.sg_query_buffer_state(buf);\n}\n\nSOKOL_GFX_API_DECL sg_resource_state\nsg_query_shader_state(sg_shader shd) {\n\treturn API.sg_query_shader_state(shd);\n}\n\nSOKOL_GFX_API_DECL sg_resource_state\nsg_query_pipeline_state(sg_pipeline pip) {\n\treturn API.sg_query_pipeline_state(pip);\n}\n\n\n\nstruct lua_api;\nstruct soluna_api;\n\nstruct extlua_apis {\n\tstruct lua_api * lua;\n\tstruct sokol_api * sokol;\n\tstruct soluna_api * soluna;\n};\n\nvoid\nsokolapi_init(lua_State *L) {\n\tstruct extlua_apis *apis = *(struct extlua_apis **)lua_getextraspace(L);\n\tAPI = *apis->sokol;\n}\n"
  },
  {
    "path": "extlua/sokolapi.temp.c",
    "content": "#include <lua.h>\n\n#include \"sokol/sokol_gfx.h\"\n\nstruct sokol_api {\n\tint version;\n\n$API_DECL$\n};\n\nstatic struct sokol_api API;\n\n$API_IMPL$\n\nstruct lua_api;\nstruct soluna_api;\n\nstruct extlua_apis {\n\tstruct lua_api * lua;\n\tstruct sokol_api * sokol;\n\tstruct soluna_api * soluna;\n};\n\nvoid\nsokolapi_init(lua_State *L) {\n\tstruct extlua_apis *apis = *(struct extlua_apis **)lua_getextraspace(L);\n\tAPI = *apis->sokol;\n}\n"
  },
  {
    "path": "extlua/sokolapi_impl.c",
    "content": "// AUTO GENERATED by sokolapi_impl.temp.c, DONT EDIT\n\n#include \"sokol/sokol_gfx.h\"\n\nstruct sokol_api {\n\tint version;\n\n\tsg_buffer (*sg_make_buffer) (const sg_buffer_desc* desc);\n\tsg_shader (*sg_make_shader) (const sg_shader_desc* desc);\n\tsg_pipeline (*sg_make_pipeline) (const sg_pipeline_desc* desc);\n\tvoid (*sg_destroy_buffer) (sg_buffer buf);\n\tvoid (*sg_destroy_shader) (sg_shader shd);\n\tvoid (*sg_destroy_pipeline) (sg_pipeline pip);\n\tvoid (*sg_update_buffer) (sg_buffer buf,  const sg_range* data);\n\tint (*sg_append_buffer) (sg_buffer buf,  const sg_range* data);\n\tvoid (*sg_apply_pipeline) (sg_pipeline pip);\n\tvoid (*sg_apply_bindings) (const sg_bindings* bindings);\n\tvoid (*sg_apply_uniforms) (int ub_slot,  const sg_range* data);\n\tvoid (*sg_draw) (int base_element,  int num_elements,  int num_instances);\n\tvoid (*sg_draw_ex) (int base_element,  int num_elements,  int num_instances,  int base_vertex,  int base_instance);\n\tsg_backend (*sg_query_backend) (void);\n\tsg_resource_state (*sg_query_buffer_state) (sg_buffer buf);\n\tsg_resource_state (*sg_query_shader_state) (sg_shader shd);\n\tsg_resource_state (*sg_query_pipeline_state) (sg_pipeline pip);\n};\n\nstruct sokol_api *\nextlua_sokol_api() {\n\tstatic struct sokol_api api = {\n\t\tSOKOL_GFX_INCLUDED,\n\n\t\tsg_make_buffer,\n\t\tsg_make_shader,\n\t\tsg_make_pipeline,\n\t\tsg_destroy_buffer,\n\t\tsg_destroy_shader,\n\t\tsg_destroy_pipeline,\n\t\tsg_update_buffer,\n\t\tsg_append_buffer,\n\t\tsg_apply_pipeline,\n\t\tsg_apply_bindings,\n\t\tsg_apply_uniforms,\n\t\tsg_draw,\n\t\tsg_draw_ex,\n\t\tsg_query_backend,\n\t\tsg_query_buffer_state,\n\t\tsg_query_shader_state,\n\t\tsg_query_pipeline_state,\n\t};\n\treturn &api;\n}\n"
  },
  {
    "path": "extlua/sokolapi_impl.temp.c",
    "content": "#include \"sokol/sokol_gfx.h\"\n\nstruct sokol_api {\n\tint version;\n\n$API_DECL$\n};\n\nstruct sokol_api *\nextlua_sokol_api() {\n\tstatic struct sokol_api api = {\n\t\tSOKOL_GFX_INCLUDED,\n\n$API_STRUCT$\n\t};\n\treturn &api;\n}\n"
  },
  {
    "path": "extlua/solunaapi.c",
    "content": "// AUTO GENERATED by solunaapi.temp.c, DONT EDIT\n\n#include \"solunaapi.h\"\n\n#include <lauxlib.h>\n\nstruct soluna_api {\n\tint version;\n\n\tsoluna_material_error (*material_submit) (const void *stream, int prim_n, int material_id, int batch_n, void *ud, soluna_material_submit_func submit);\n\tint (*material_sprite_rect) (struct soluna_sprite_bank bank, int sprite, struct soluna_sprite_rect *out);\n\tsg_bindings (*material_bindings) (struct soluna_render_bindings bindings);\n\tsoluna_material_error (*material_push_stream) (int material_id, int count, size_t payload_size, soluna_material_stream_write_func write, void *ud, struct soluna_material_stream *out);\n\tvoid (*material_stream_free) (void *ptr);\n\tint (*material_stream_read) (struct soluna_material_stream_context ctx, int index, size_t payload_size, void *payload, struct soluna_material_stream_data *out);\n\tvoid (*material_stream_error) (struct soluna_material_stream_context ctx, const char *error);\n\tint (*material_stream_failed) (struct soluna_material_stream_context ctx);\n};\n\nstatic struct soluna_api API;\n\nsoluna_material_error\nsoluna_material_submit(const void *stream, int prim_n, int material_id, int batch_n, void *ud, soluna_material_submit_func submit) {\n\treturn API.material_submit(stream, prim_n, material_id, batch_n, ud, submit);\n}\n\nint\nsoluna_material_sprite_rect(struct soluna_sprite_bank bank, int sprite, struct soluna_sprite_rect *out) {\n\treturn API.material_sprite_rect(bank, sprite, out);\n}\n\nsg_bindings\nsoluna_material_bindings(struct soluna_render_bindings bindings) {\n\treturn API.material_bindings(bindings);\n}\n\nsoluna_material_error\nsoluna_material_push_stream(int material_id, int count, size_t payload_size, soluna_material_stream_write_func write, void *ud, struct soluna_material_stream *out) {\n\treturn API.material_push_stream(material_id, count, payload_size, write, ud, out);\n}\n\nvoid\nsoluna_material_stream_free(void *ptr) {\n\tAPI.material_stream_free(ptr);\n}\n\nint\nsoluna_material_stream_read(struct soluna_material_stream_context ctx, int index, size_t payload_size, void *payload, struct soluna_material_stream_data *out) {\n\treturn API.material_stream_read(ctx, index, payload_size, payload, out);\n}\n\nvoid\nsoluna_material_stream_error(struct soluna_material_stream_context ctx, const char *error) {\n\tAPI.material_stream_error(ctx, error);\n}\n\nint\nsoluna_material_stream_failed(struct soluna_material_stream_context ctx) {\n\treturn API.material_stream_failed(ctx);\n}\n\n\n\nstruct lua_api;\nstruct sokol_api;\n\nstruct extlua_apis {\n\tstruct lua_api * lua;\n\tstruct sokol_api * sokol;\n\tstruct soluna_api * soluna;\n};\n\nvoid\nsolunaapi_init(lua_State *L) {\n\tstruct extlua_apis *apis = *(struct extlua_apis **)lua_getextraspace(L);\n\tif (apis == NULL || apis->soluna == NULL || apis->soluna->version != SOLUNA_EXT_API_VERSION) {\n\t\tint version = (apis != NULL && apis->soluna != NULL) ? apis->soluna->version : 0;\n\t\tluaL_error(L, \"soluna ext api version mismatch, expected %d got %d\", SOLUNA_EXT_API_VERSION, version);\n\t}\n\tAPI = *apis->soluna;\n}\n"
  },
  {
    "path": "extlua/solunaapi.h",
    "content": "// AUTO GENERATED by solunaapi.h.temp, DONT EDIT\n\n#ifndef SOLUNAAPI_H\n#define SOLUNAAPI_H\n\n#include <stddef.h>\n\n#include <lua.h>\n\n#include \"sokol/sokol_gfx.h\"\n\n#define SOLUNA_EXT_API_VERSION 1\n\nstruct soluna_sprite_rect {\n\tint texture;\n\tfloat u;\n\tfloat v;\n\tfloat w;\n\tfloat h;\n\tfloat ox;\n\tfloat oy;\n};\n\nstruct soluna_material_stream_item {\n\tfloat x;\n\tfloat y;\n\tint sprite;\n\tconst void *payload;\n};\n\nstruct soluna_material_stream_data {\n\tfloat x;\n\tfloat y;\n\tint sprite;\n};\n\nstruct soluna_material_stream {\n\tchar *data;\n\tsize_t size;\n};\n\ntypedef const char *soluna_material_error;\n\nstruct soluna_material_stream_context {\n\tvoid *ctx;\n};\n\nstruct soluna_render_bindings {\n\tvoid *ctx;\n};\n\nstruct soluna_sprite_bank {\n\tvoid *ctx;\n};\n\ntypedef void (*soluna_material_submit_func)(void *ud, struct soluna_material_stream_context ctx, int n);\ntypedef void (*soluna_material_stream_write_func)(void *ud, int index, struct soluna_material_stream_item *item);\n\n\nvoid solunaapi_init(lua_State *L);\nsoluna_material_error soluna_material_submit(const void *stream, int prim_n, int material_id, int batch_n, void *ud, soluna_material_submit_func submit);\nint soluna_material_sprite_rect(struct soluna_sprite_bank bank, int sprite, struct soluna_sprite_rect *out);\nsg_bindings soluna_material_bindings(struct soluna_render_bindings bindings);\nsoluna_material_error soluna_material_push_stream(int material_id, int count, size_t payload_size, soluna_material_stream_write_func write, void *ud, struct soluna_material_stream *out);\nvoid soluna_material_stream_free(void *ptr);\nint soluna_material_stream_read(struct soluna_material_stream_context ctx, int index, size_t payload_size, void *payload, struct soluna_material_stream_data *out);\nvoid soluna_material_stream_error(struct soluna_material_stream_context ctx, const char *error);\nint soluna_material_stream_failed(struct soluna_material_stream_context ctx);\n\n#endif\n"
  },
  {
    "path": "extlua/solunaapi.h.temp",
    "content": "#ifndef SOLUNAAPI_H\n#define SOLUNAAPI_H\n\n#include <stddef.h>\n\n#include <lua.h>\n\n#include \"sokol/sokol_gfx.h\"\n\n$TYPE_DECL$\n\n$HEADER_DECL$\n\n#endif\n"
  },
  {
    "path": "extlua/solunaapi.temp.c",
    "content": "#include \"solunaapi.h\"\n\n#include <lauxlib.h>\n\nstruct soluna_api {\n\tint version;\n\n$API_DECL$\n};\n\nstatic struct soluna_api API;\n\n$API_IMPL$\n\nstruct lua_api;\nstruct sokol_api;\n\nstruct extlua_apis {\n\tstruct lua_api * lua;\n\tstruct sokol_api * sokol;\n\tstruct soluna_api * soluna;\n};\n\nvoid\nsolunaapi_init(lua_State *L) {\n\tstruct extlua_apis *apis = *(struct extlua_apis **)lua_getextraspace(L);\n\tif (apis == NULL || apis->soluna == NULL || apis->soluna->version != SOLUNA_EXT_API_VERSION) {\n\t\tint version = (apis != NULL && apis->soluna != NULL) ? apis->soluna->version : 0;\n\t\tluaL_error(L, \"soluna ext api version mismatch, expected %d got %d\", SOLUNA_EXT_API_VERSION, version);\n\t}\n\tAPI = *apis->soluna;\n}\n"
  },
  {
    "path": "extlua/solunaapi_impl.c",
    "content": "// AUTO GENERATED by solunaapi_impl.temp.c, DONT EDIT\n\n#include <stddef.h>\n\n#include \"sokol/sokol_gfx.h\"\n\n#define SOLUNA_EXT_API_VERSION 1\n\nstruct soluna_sprite_rect {\n\tint texture;\n\tfloat u;\n\tfloat v;\n\tfloat w;\n\tfloat h;\n\tfloat ox;\n\tfloat oy;\n};\n\nstruct soluna_material_stream_item {\n\tfloat x;\n\tfloat y;\n\tint sprite;\n\tconst void *payload;\n};\n\nstruct soluna_material_stream_data {\n\tfloat x;\n\tfloat y;\n\tint sprite;\n};\n\nstruct soluna_material_stream {\n\tchar *data;\n\tsize_t size;\n};\n\ntypedef const char *soluna_material_error;\n\nstruct soluna_material_stream_context {\n\tvoid *ctx;\n};\n\nstruct soluna_render_bindings {\n\tvoid *ctx;\n};\n\nstruct soluna_sprite_bank {\n\tvoid *ctx;\n};\n\ntypedef void (*soluna_material_submit_func)(void *ud, struct soluna_material_stream_context ctx, int n);\ntypedef void (*soluna_material_stream_write_func)(void *ud, int index, struct soluna_material_stream_item *item);\n\n\nextern soluna_material_error material_submit(const void *stream, int prim_n, int material_id, int batch_n, void *ud, soluna_material_submit_func submit);\nextern int material_sprite_rect(struct soluna_sprite_bank bank, int sprite, struct soluna_sprite_rect *out);\nextern sg_bindings material_bindings(struct soluna_render_bindings bindings);\nextern soluna_material_error material_push_stream(int material_id, int count, size_t payload_size, soluna_material_stream_write_func write, void *ud, struct soluna_material_stream *out);\nextern void material_stream_free(void *ptr);\nextern int material_stream_read(struct soluna_material_stream_context ctx, int index, size_t payload_size, void *payload, struct soluna_material_stream_data *out);\nextern void material_stream_error(struct soluna_material_stream_context ctx, const char *error);\nextern int material_stream_failed(struct soluna_material_stream_context ctx);\n\nstruct soluna_api {\n\tint version;\n\n\tsoluna_material_error (*material_submit) (const void *stream, int prim_n, int material_id, int batch_n, void *ud, soluna_material_submit_func submit);\n\tint (*material_sprite_rect) (struct soluna_sprite_bank bank, int sprite, struct soluna_sprite_rect *out);\n\tsg_bindings (*material_bindings) (struct soluna_render_bindings bindings);\n\tsoluna_material_error (*material_push_stream) (int material_id, int count, size_t payload_size, soluna_material_stream_write_func write, void *ud, struct soluna_material_stream *out);\n\tvoid (*material_stream_free) (void *ptr);\n\tint (*material_stream_read) (struct soluna_material_stream_context ctx, int index, size_t payload_size, void *payload, struct soluna_material_stream_data *out);\n\tvoid (*material_stream_error) (struct soluna_material_stream_context ctx, const char *error);\n\tint (*material_stream_failed) (struct soluna_material_stream_context ctx);\n};\n\nstruct soluna_api *\nextlua_soluna_api() {\n\tstatic struct soluna_api api = {\n\t\tSOLUNA_EXT_API_VERSION,\n\n\t\tmaterial_submit,\n\t\tmaterial_sprite_rect,\n\t\tmaterial_bindings,\n\t\tmaterial_push_stream,\n\t\tmaterial_stream_free,\n\t\tmaterial_stream_read,\n\t\tmaterial_stream_error,\n\t\tmaterial_stream_failed,\n\t};\n\treturn &api;\n}\n"
  },
  {
    "path": "extlua/solunaapi_impl.temp.c",
    "content": "$HOST_TYPE_DECL$\n\n$API_EXTERN$\n\nstruct soluna_api {\n\tint version;\n\n$API_DECL$\n};\n\nstruct soluna_api *\nextlua_soluna_api() {\n\tstatic struct soluna_api api = {\n\t\tSOLUNA_EXT_API_VERSION,\n\n$API_STRUCT$\n\t};\n\treturn &api;\n}\n"
  },
  {
    "path": "make.lua",
    "content": "local lm = require \"luamake\"\nlocal fs = require \"bee.filesystem\"\n\nlocal function detect_emcc()\n\tif lm.compiler == \"emcc\" then\n\t\treturn true\n\tend\n\tif type(lm.cc) == \"string\" and lm.cc:find(\"emcc\", 1, true) then\n\t\treturn true\n\tend\n\treturn false\nend\n\nlocal osplat = (function()\n\tif lm.os == \"windows\" then\n\t\tif lm.compiler == \"gcc\" then\n\t\t\treturn \"mingw\"\n\t\tend\n\t\tif lm.cc == \"clang-cl\" then\n\t\t\treturn \"clang-cl\"\n\t\tend\n\t\treturn \"msvc\"\n\tend\n\treturn lm.os\nend)()\n\nlocal plat = (function()\n\tif detect_emcc() then\n\t\treturn \"emcc\"\n\tend\n\treturn osplat\nend)()\n\nlm.platform = plat\nlm.basedir = lm:path \".\"\nlm.bindir = (\"bin/%s/%s\"):format(plat, lm.mode)\nlm.osbindir = (\"bin/%s/%s\"):format(osplat, lm.mode)\n\nlm:conf {\n\tcxx = \"c++20\",\n\tclang = {\n\t\tc = \"c11\",\n\t},\n\tflags = {\n\t\tlm.mode ~= \"debug\" and \"-O2\",\n\t},\n\tmsvc = {\n\t\tc = \"c11\",\n\t\tflags = {\n\t\t\t\"-W3\",\n\t\t\t\"-utf-8\",\n\t\t\t\"-experimental:c11atomics\",\n\t\t\t\"/wd4244\",\n\t\t\t\"/wd4267\",\n\t\t\t\"/wd4305\",\n\t\t\t\"/wd4996\",\n\t\t\t\"/wd4018\",\n\t\t\t\"/wd4113\",\n\t\t},\n\t\tdefines = {\n\t\t\t\"_CRT_SECURE_NO_WARNINGS\",\n\t\t\t\"_CRT_NONSTDC_NO_DEPRECATE\",\n\t\t\t\"_CRT_SECURE_NO_DEPRECATE\"\n\t\t},\n\t},\n\tmingw = {\n\t\tc = \"c99\",\n\t},\n\tgcc = {\n\t\tc = \"c11\",\n\t\tflags = {\n\t\t\t\"-Wall\",\n\t\t},\n\t\tdefines = {\n\t\t\t\"_POSIX_C_SOURCE=199309L\",\n\t\t\t\"_GNU_SOURCE\",\n\t\t},\n\t\tlinks = {\n\t\t\t\"m\",\n\t\t\t(lm.os ~= \"windows\" and lm.platform ~= \"emcc\") and \"fontconfig\",\n\t\t},\n\t},\n\temcc = {\n\t\tc = \"gnu11\",\n\t\tflags = {\n\t\t\t\"-Wall\",\n\t\t\t\"-pthread\",\n\t\t\t\"-fPIC\",\n\t\t\t\"--use-port=emdawnwebgpu\",\n\t\t\t\"-fwasm-exceptions\",\n\t\t},\n\t\tlinks = {\n\t\t\t\"idbfs.js\",\n\t\t},\n\t\tldflags = {\n\t\t\t\"--use-port=emdawnwebgpu\",\n\t\t\t\"-s ALLOW_MEMORY_GROWTH\",\n\t\t\t\"-s FORCE_FILESYSTEM=1\",\n\t\t\t\"-s USE_PTHREADS=1\",\n\t\t\t\"-fwasm-exceptions\",\n\n\t\t\tlm.mode == \"debug\" and \"-gsource-map\",\n\t\t\tlm.mode == \"debug\" and \"-s EXCEPTION_STACK_TRACES=1\",\n\t\t\tlm.mode == \"debug\" and \"-s ASSERTIONS=2\",\n\t\t\t-- lm.mode == \"debug\" and \"-s SAFE_HEAP=1\",\n\t\t\tlm.mode == \"debug\" and \"-s STACK_OVERFLOW_CHECK=1\",\n\t\t\tlm.mode == \"debug\" and \"-s PTHREADS_DEBUG=1\",\n\t\t},\n\t\tdefines = {\n\t\t\t\"_POSIX_C_SOURCE=200809L\",\n\t\t\t\"_GNU_SOURCE\",\n\t\t},\n\t},\n\tdefines = {\n\t\t-- lm.mode == \"debug\" and \"DEBUGLOG\",\n\t\tlm.mode == \"debug\" and \"SOKOL_DEBUG\",\n\t}\n}\n\nlocal deps = { \"soluna_src\" }\n\nfor path in fs.pairs(lm.basedir .. \"/clibs\") do\n\tlocal name = path:stem():string()\n\tif name ~= \"soluna\" and name ~= \"sample\" and fs.exists(path / \"make.lua\") then\n\t\tlocal makefile = (\"clibs/%s/make.lua\"):format(name)\n\t\tlm:import(makefile)\n\t\tdeps[#deps + 1] = name .. \"_src\"\n\tend\nend\n\nlm:import \"clibs/soluna/make.lua\"\nlm:import \"clibs/sample/make.lua\"\n\nlm:exe \"soluna\" {\n\tdeps = deps,\n\temcc = {\n\t\tldflags = {\n\t\t\t\"--js-library=src/platform/wasm/soluna_ime.js\",\n\t\t\t\"--js-library=src/platform/wasm/soluna_openurl.js\",\n\t\t\t\"-s MODULARIZE=1\",\n\t\t\t\"-s EXPORT_ES6=1\",\n\t\t\t\"-s EXPORT_NAME=createApp\",\n\t\t\t'-s EXPORTED_FUNCTIONS=\\'[\"_main\"]\\'',\n\t\t\t'-s EXPORTED_RUNTIME_METHODS=\\'[\"FS\",\"FS_createPath\",\"FS_createDataFile\",\"IDBFS\"]\\'',\n\t\t\t\"-s PTHREAD_POOL_SIZE='Math.max(2,navigator.hardwareConcurrency)'\",\n\t\t\t\"-s PTHREAD_POOL_SIZE_STRICT=2\",\n\t\t\t\"-s MAIN_MODULE=2\",\n\t\t\t\"-Wl,-u,emscripten_builtin_memalign\",\n\t\t\t\"-Wl,--export=emscripten_builtin_memalign\",\n\t\t},\n\t},\n}\n\nlm:import \"script/act_targets.lua\"\n\nlm:runlua \"cc\" {\n\tscript = \"script/compile_commands.lua\",\n\targs = {\n\t\t\"build/build.ninja\",\n\t\t\"compile_commands.json\",\n\t},\n\tinputs = {\n\t\t\"build/build.ninja\",\n\t\t\"script/compile_commands.lua\",\n\t},\n\toutputs = {\n\t\t\"compile_commands.json\",\n\t},\n}\n\nlm:default \"soluna\"\n"
  },
  {
    "path": "script/act.lua",
    "content": "local platform = require \"bee.platform\"\nlocal fs = require \"bee.filesystem\"\nlocal subprocess = require \"bee.subprocess\"\n\nlocal is_windows = platform.os == \"windows\"\n\nlocal function quote_ps(value)\n    return \"'\" .. value:gsub(\"'\", \"''\") .. \"'\"\nend\n\nlocal function cmdline(args)\n    local out = {}\n    for i = 1, #args do\n        local v = args[i]\n        if v:find(\"[%s\\\"]\") then\n            out[#out + 1] = '\"' .. v:gsub('\"', '\\\\\"') .. '\"'\n        else\n            out[#out + 1] = v\n        end\n    end\n    return table.concat(out, \" \")\nend\n\nlocal function run(args, option)\n    option = option or {}\n    print(\"> \" .. cmdline(args))\n    local process, errmsg = subprocess.spawn {\n        args,\n        searchPath = true,\n        stdout = option.stdout ~= nil and option.stdout or io.stdout,\n        stderr = option.stderr ~= nil and option.stderr or \"stdout\",\n    }\n    assert(process, errmsg)\n    local code = process:wait()\n    process:detach()\n    if code ~= 0 then\n        error((\"command failed (%d): %s\"):format(code, cmdline(args)))\n    end\nend\n\nlocal function run_code(args)\n    local process = assert(subprocess.spawn {\n        args,\n        searchPath = true,\n        stdout = true,\n        stderr = true,\n    })\n    local code = process:wait()\n    process:detach()\n    return code\nend\n\nlocal function exists(path)\n    return fs.exists(path)\nend\n\nlocal function getenv(name, default)\n    local value = os.getenv(name)\n    if value == nil or value == \"\" then\n        return default\n    end\n    return value\nend\n\nlocal function reset_dir(path)\n    pcall(fs.remove_all, path)\n    if not exists(path) then\n        fs.create_directories(path)\n    end\nend\n\nlocal function get_home()\n    if is_windows then\n        local userprofile = os.getenv(\"USERPROFILE\")\n        if userprofile and userprofile ~= \"\" then\n            return fs.path(userprofile)\n        end\n        local home_drive = os.getenv(\"HOMEDRIVE\") or \"\"\n        local home_path = os.getenv(\"HOMEPATH\") or \"\"\n        local combined = home_drive .. home_path\n        if combined ~= \"\" then\n            return fs.path(combined)\n        end\n        return nil\n    end\n    local home = os.getenv(\"HOME\")\n    if home and home ~= \"\" then\n        return fs.path(home)\n    end\n    return nil\nend\n\nlocal function detect_python()\n    if is_windows then\n        if run_code { \"where\", \"py\" } == 0 then\n            return { \"py\", \"-3\" }\n        end\n        if run_code { \"where\", \"python\" } == 0 then\n            return { \"python\" }\n        end\n    else\n        if run_code { \"which\", \"python3\" } == 0 then\n            return { \"python3\" }\n        end\n        if run_code { \"which\", \"python\" } == 0 then\n            return { \"python\" }\n        end\n    end\n    error \"python interpreter not found\"\nend\n\nlocal function find_file(root, filename)\n    for path, status in fs.pairs_r(root) do\n        if status and status:is_regular_file() and path:filename():string() == filename then\n            return path\n        end\n    end\n    return nil\nend\n\nlocal function collect_files(root)\n    local files = {}\n    if not exists(root) then\n        return files\n    end\n    for path, status in fs.pairs_r(root) do\n        if status and status:is_regular_file() then\n            files[#files + 1] = path\n        end\n    end\n    table.sort(files, function(a, b)\n        return a:string() < b:string()\n    end)\n    return files\nend\n\nlocal workflow = (arg[1] or \"pages\"):lower()\nlocal workflow_file = ({\n    pages = \".github/workflows/pages.yml\",\n    nightly = \".github/workflows/nightly.yml\",\n})[workflow]\n\nif not workflow_file then\n    error((\"unknown workflow: %s (expected: pages or nightly)\"):format(workflow))\nend\n\nlocal function parse_options(argv)\n    local options = {\n        port = tonumber(getenv(\"PORT\", \"8080\")) or 8080,\n        host_os = platform.os,\n    }\n    for i = 2, #argv do\n        local value = argv[i]\n        local port_num = tonumber(value)\n        local kv_key, kv_value = value:match(\"^([%w_]+)=(.+)$\")\n        if kv_key == \"port\" then\n            local p = tonumber(kv_value)\n            if p then\n                options.port = p\n            end\n        elseif kv_key == \"host_os\" then\n            options.host_os = kv_value:lower()\n        elseif port_num then\n            options.port = port_num\n        end\n    end\n    return options\nend\n\nlocal options = parse_options(arg)\nlocal port = options.port\nlocal home = assert(get_home(), \"home directory is not set\")\nlocal root = fs.path(getenv(\"ACT_ROOT\", (home / \".act/soluna\"):string()))\nlocal artifact_root = root / \"artifacts\"\nlocal unpack_dir = root / \"unpack\"\nlocal serve_root = root / \"serve\"\nlocal preview_dir = serve_root / \"soluna\"\n\nreset_dir(artifact_root)\nreset_dir(unpack_dir)\nreset_dir(preview_dir)\n\nlocal act_args = {\n    \"act\",\n    \"workflow_dispatch\",\n    \"-W\",\n    workflow_file,\n    \"--container-architecture\",\n    \"linux/amd64\",\n    \"--artifact-server-path\",\n    artifact_root:string(),\n}\nif workflow == \"nightly\" then\n    local matrix_os = ({\n        windows = \"windows-latest\",\n        macos = \"macos-latest\",\n    })[options.host_os] or \"ubuntu-latest\"\n    act_args[#act_args + 1] = \"--matrix\"\n    act_args[#act_args + 1] = \"os:\" .. matrix_os\n    if matrix_os == \"windows-latest\" or matrix_os == \"macos-latest\" then\n        act_args[#act_args + 1] = \"-P\"\n        act_args[#act_args + 1] = matrix_os .. \"=-self-hosted\"\n    end\n    print((\"Nightly matrix selected: %s (host_os=%s)\"):format(matrix_os, options.host_os))\nend\nrun(act_args)\n\nif workflow ~= \"pages\" then\n    print(\"Workflow completed: \" .. workflow)\n    print(\"Artifacts root: \" .. artifact_root:string())\n    local files = collect_files(artifact_root)\n    if #files == 0 then\n        print(\"No artifact files were found under artifacts root.\")\n    else\n        print(\"Artifact files:\")\n        for i = 1, #files do\n            local rel = files[i]:string():sub(#artifact_root:string() + 2)\n            print(\" - \" .. rel)\n        end\n    end\n    return\nend\n\nlocal zip_path = find_file(artifact_root, \"github-pages.zip\")\nif not zip_path then\n    error(table.concat({\n        \"missing github-pages.zip under: \" .. artifact_root:string(),\n        \"The workflow build job was likely skipped (for example, unsupported runner image).\",\n        \"You can inspect with: act -l ; act -n -W .github/workflows/pages.yml\",\n        \"If needed, pass platform mapping manually, for example:\",\n        \"  act workflow_dispatch -W .github/workflows/pages.yml -P ubuntu-latest=ghcr.io/catthehacker/ubuntu:act-latest\",\n    }, \"\\n\"))\nend\n\nif is_windows then\n    run {\n        \"powershell\",\n        \"-NoProfile\",\n        \"-Command\",\n        \"Expand-Archive -LiteralPath \"\n            .. quote_ps(zip_path:string())\n            .. \" -DestinationPath \"\n            .. quote_ps(unpack_dir:string())\n            .. \" -Force\",\n    }\nelse\n    run {\n        \"unzip\",\n        \"-o\",\n        zip_path:string(),\n        \"-d\",\n        unpack_dir:string(),\n    }\nend\n\nlocal artifact_tar = find_file(unpack_dir, \"artifact.tar\")\nif not artifact_tar then\n    error(\"missing artifact.tar under: \" .. unpack_dir:string())\nend\n\nrun {\n    \"tar\",\n    \"-xf\",\n    artifact_tar:string(),\n    \"-C\",\n    preview_dir:string(),\n}\n\nprint(\"Preview files are ready at: \" .. preview_dir:string())\nprint(\"Serving http://127.0.0.1:\" .. port .. \"/soluna/\")\nprint \"If the page loads before the service worker takes control, refresh once.\"\n\nlocal python = detect_python()\npython[#python + 1] = \"-m\"\npython[#python + 1] = \"http.server\"\npython[#python + 1] = tostring(port)\npython[#python + 1] = \"--directory\"\npython[#python + 1] = serve_root:string()\nrun(python)\n"
  },
  {
    "path": "script/act_targets.lua",
    "content": "local lm = require \"luamake\"\n\nlm:rule \"act_lua\" {\n    args = { \"$luamake\", \"lua\", \"$args\" },\n    description = \"$args\",\n    pool = \"console\",\n}\n\nlm:build \"pages\" {\n    rule = \"act_lua\",\n    args = { \"@act.lua\", \"pages\" },\n    inputs = { \"act.lua\" },\n}\n\nlm:build \"nightly\" {\n    rule = \"act_lua\",\n    args = { \"@act.lua\", \"nightly\", \"host_os=\" .. lm.os },\n    inputs = { \"act.lua\" },\n}\n\nlm:phony \"act\" {\n    inputs = \"act.lua\",\n}\n"
  },
  {
    "path": "script/compile_commands.lua",
    "content": "local subprocess = require \"bee.subprocess\"\n\nlocal ninja_file, output = ...\n\nassert(ninja_file, \"missing ninja file path\")\nassert(output, \"missing output path\")\n\nlocal process = assert(subprocess.spawn {\n\t\"ninja\",\n\t\"-f\",\n\tninja_file,\n\t\"-t\",\n\t\"compdb\",\n\t\"-x\",\n\tsearchPath = true,\n\tstdout = true,\n})\n\nlocal content = process.stdout:read \"a\"\nlocal code = process:wait()\nif code ~= 0 then\n\tos.exit(code, true)\nend\n\nlocal file <close> = assert(io.open(output, \"wb\"))\nfile:write(content)\n"
  },
  {
    "path": "script/datalist2c.lua",
    "content": "local dlsrc, cname = ...\n\nlocal f = assert(io.open(dlsrc,\"rb\"))\nlocal bin = f:read \"a\"\nf:close()\n\nlocal code = [[\nstatic const unsigned char dl_$name[] = {\n\t$bytes\n};\n]]\n\nlocal count = 0\nlocal function tohex(c)\n\tlocal b = string.format(\"0x%02x,\", c:byte())\n\tcount = count + 1\n\tif count == 16 then\n\t\tb = b .. \"\\n\\t\"\n\t\tcount = 0\n\tend\n\treturn b\nend\n\nlocal p = {\n\tname = cname:match \"([^/]+)%.dl%.h$\",\n\tbytes = string.gsub(bin .. \"\\0\", \".\", tohex),\t\n}\n\nlocal source = string.gsub(code, \"$(%w+)\", p)\n\nlocal f = assert(io.open(cname, \"w\"))\nf:write(source)\nf:close()"
  },
  {
    "path": "script/hashversion.lua",
    "content": "local function get_rev()\n\tlocal f = io.popen \"git rev-parse HEAD\"\n\tlocal rev = f:read \"a\"\n\tf:close()\n\treturn rev:match \"[%da-f]*\"\nend\n\nlocal version = get_rev()\nprint(\"SOLUNA_HASH_VERSION\", version)\n\nlocal f = assert(io.open \"src/version.h\")\nlocal text = f:read \"a\"\ntext = text:gsub('(SOLUNA_HASH_VERSION%s+\")([%da-f]*)(\")', \"%1\".. version .. \"%3\")\nf:close()\nlocal f = assert(io.open(\"src/version.h\", \"wb\"))\nf:write(text)\nf:close()\n"
  },
  {
    "path": "script/lua2c.lua",
    "content": "local luasrc, cname = ...\n\nlocal s = assert(loadfile(luasrc))\nlocal bin = string.dump(s)\n\nlocal code = [[\nstatic const unsigned char luasrc_$name[] = {\n\t$bytes\n};\n]]\n\nlocal count = 0\nlocal function tohex(c)\n\tlocal b = string.format(\"0x%02x,\", c:byte())\n\tcount = count + 1\n\tif count == 16 then\n\t\tb = b .. \"\\n\\t\"\n\t\tcount = 0\n\tend\n\treturn b\nend\n\nlocal p = {\n\tname = cname:match \"([^/]+)%.lua%.h$\",\n\tbytes = string.gsub(bin .. \"\\0\" , \".\", tohex),\t\n}\n\nlocal source = string.gsub(code, \"$(%w+)\", p)\n\nlocal f = assert(io.open(cname, \"w\"))\nf:write(source)\nf:close()"
  },
  {
    "path": "src/appevent.h",
    "content": "#ifndef soluna_app_event_h\n#define soluna_app_event_h\n\nstruct event_message {\n\tconst char *typestr;\n\tint p1;\n\tint p2;\n\tint p3;\n};\n\nstatic inline void\nget_xy(struct event_message *em, float x, float y) {\n\tfloat dpi_scale = sapp_dpi_scale();\n\tfloat inv;\n\tif (dpi_scale <= 0.0f) {\n\t\tdpi_scale = 1.0f;\n\t\tinv = 1.0f;\n\t} else {\n\t\tinv = 1.0f / dpi_scale;\n\t}\n\tfloat logical_x = x * inv;\n\tfloat logical_y = y * inv;\n\tif (logical_x >= 0.0f)\n\t\tem->p1 = (int)(logical_x + 0.5f);\n\telse\n\t\tem->p1 = (int)(logical_x - 0.5f);\n\tif (logical_y >= 0.0f)\n\t\tem->p2 = (int)(logical_y + 0.5f);\n\telse\n\t\tem->p2 = (int)(logical_y - 0.5f);\n}\n\nstatic inline void\nmouse_message(struct event_message *em, const sapp_event* ev) {\n\tswitch (ev->type) {\n\tcase SAPP_EVENTTYPE_MOUSE_MOVE:\n\t\tem->typestr = \"mouse_move\";\n\t\tget_xy(em, ev->mouse_x, ev->mouse_y);\n\t\tbreak;\n\tcase SAPP_EVENTTYPE_MOUSE_DOWN:\n\tcase SAPP_EVENTTYPE_MOUSE_UP:\n\t\tem->typestr = \"mouse_button\";\n\t\tem->p1 = ev->mouse_button;\n\t\tem->p2 = ev->type == SAPP_EVENTTYPE_MOUSE_DOWN;\n\t\tbreak;\n\tcase SAPP_EVENTTYPE_MOUSE_SCROLL:\n\t\tem->typestr = \"mouse_scroll\";\n\t\tem->p1 = ev->scroll_y;\n\t\tem->p2 = ev->scroll_x;\n\t\tbreak;\n\tdefault:\n\t\tem->typestr = \"mouse\";\n\t\tem->p1 = ev->type;\n\t\tbreak;\n\t}\n}\n\nstatic inline void\ntouch_message(struct event_message *em, const sapp_event* ev) {\n\t// todo : support multi touch points\n\t// get 1st touch point now\n    const sapp_touchpoint *t = &ev->touches[0];\n\tget_xy(em, t->pos_x, t->pos_y);\n\tem->p3 = t->changed;\n\tswitch (ev->type) {\n\tcase SAPP_EVENTTYPE_TOUCHES_BEGAN:\n\t\tem->typestr = \"touch_begin\";\n\t\tbreak;\n\tcase SAPP_EVENTTYPE_TOUCHES_MOVED:\n\t\tem->typestr = \"touch_moved\";\n\t\tbreak;\n\tcase SAPP_EVENTTYPE_TOUCHES_ENDED:\n\t\tem->typestr = \"touch_end\";\n\t\tbreak;\n\tcase SAPP_EVENTTYPE_TOUCHES_CANCELLED:\n\t\tem->typestr = \"touch_cancelled\";\n\t\tbreak;\n\tdefault:\n\t\tem->typestr = \"touch\";\n\t\tbreak;\n\t}\n}\n\nstatic inline void\nwindow_message(struct event_message *em, const sapp_event *ev) {\n\tswitch (ev->type) {\n\tcase SAPP_EVENTTYPE_RESIZED:\n\t\tem->typestr = \"window_resize\";\n\t\tem->p1 = ev->window_width;\n\t\tem->p2 = ev->window_height;\n\t\tbreak;\n\tdefault:\n\t\tem->typestr = \"window\";\n\t\tem->p1 = ev->type;\n\t\tbreak;\n\t}\n}\n\nstatic inline void\nkey_message(struct event_message *em, const sapp_event *ev) {\n\tswitch (ev->type) {\n\tcase SAPP_EVENTTYPE_CHAR:\n\t\tem->typestr = \"char\";\n\t\tem->p1 = (int)ev->char_code;\n\t\tem->p2 = 0;\n\t\tbreak;\n\tdefault:\n\t\tem->typestr = \"key\";\n\t\tem->p1 = (int)ev->key_code;\n\t\tem->p2 = ev->type == SAPP_EVENTTYPE_KEY_DOWN;\n\t\tbreak;\n\t}\n}\n\nstatic inline void\napp_event_unpack(struct event_message *em, const sapp_event* ev) {\n\tem->typestr = NULL;\n\tem->p1 = 0;\n\tem->p2 = 0;\n\tem->p3 = 0;\n\tswitch (ev->type) {\n\tcase SAPP_EVENTTYPE_MOUSE_MOVE:\n\tcase SAPP_EVENTTYPE_MOUSE_DOWN:\n\tcase SAPP_EVENTTYPE_MOUSE_UP:\n\tcase SAPP_EVENTTYPE_MOUSE_SCROLL:\n\tcase SAPP_EVENTTYPE_MOUSE_ENTER:\n\tcase SAPP_EVENTTYPE_MOUSE_LEAVE:\n\t\tmouse_message(em, ev);\n\t\tbreak;\n\tcase SAPP_EVENTTYPE_TOUCHES_BEGAN:\n\tcase SAPP_EVENTTYPE_TOUCHES_MOVED:\n\tcase SAPP_EVENTTYPE_TOUCHES_ENDED:\n\tcase SAPP_EVENTTYPE_TOUCHES_CANCELLED:\n\t\ttouch_message(em, ev);\n\t\tbreak;\n\tcase SAPP_EVENTTYPE_RESIZED:\n\t\twindow_message(em, ev);\n\t\tbreak;\n\tcase SAPP_EVENTTYPE_CHAR:\n\tcase SAPP_EVENTTYPE_KEY_DOWN:\n\tcase SAPP_EVENTTYPE_KEY_UP:\n\t\tkey_message(em, ev);\n\t\tbreak;\n\tdefault:\n\t\tem->typestr = \"message\";\n\t\tem->p1 = ev->type;\n\t\tbreak;\n\t}\n}\n\n#endif\n"
  },
  {
    "path": "src/audio.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n\n#include \"zipreader.h\"\n\n#ifdef __EMSCRIPTEN__\n#include <emscripten.h>\n#endif\n\n#define MA_NO_WIN32_FILEIO\n#define MA_NO_MP3\n#define MA_NO_FLAC\n#define MINIAUDIO_IMPLEMENTATION\n#include \"miniaudio.h\"\n\nFILE * fopen_utf8(const char *filename, const char *mode);\n\nstatic ma_result\nvfs_open_local(ma_vfs* pVFS, const char* pFilePath, ma_uint32 openMode, ma_vfs_file* pFile) {\n\tFILE* pFileStd;\n\tconst char* pOpenModeStr;\n\n\tMA_ASSERT(pFilePath != NULL);\n\tMA_ASSERT(openMode  != 0);\n\tMA_ASSERT(pFile     != NULL);\n\n\t(void)pVFS;\n\n\tif ((openMode & MA_OPEN_MODE_READ) != 0) {\n\t\tif ((openMode & MA_OPEN_MODE_WRITE) != 0) {\n\t\t\tpOpenModeStr = \"r+\";\n\t\t} else {\n\t\t\tpOpenModeStr = \"rb\";\n\t\t}\n\t} else {\n\t\tpOpenModeStr = \"wb\";\n\t}\n\t\n\tpFileStd = fopen_utf8(pFilePath, pOpenModeStr);\n\t\n\tif (pFileStd == NULL) {\n\t\treturn MA_ERROR;\n\t}\n\n    *pFile = pFileStd;\n\n    return MA_SUCCESS;\n}\n\nstruct custom_vfs {\n\tma_default_vfs base;\n\tstruct zipreader_name *zipnames;\n};\n\nstruct custom_engine {\n\tstruct ma_engine engine;\n\tstruct ma_resource_manager rm;\n\tstruct custom_vfs vfs;\n};\n\nstruct audio_group {\n\tma_sound_group group;\n\tint alive;\n};\n\nstruct audio_sound {\n\tma_sound sound;\n\tint alive;\n};\n\n#define AUDIO_GROUP_METATABLE \"SOLUNA_AUDIO_GROUP\"\n#define AUDIO_SOUND_METATABLE \"SOLUNA_AUDIO_SOUND\"\n\nstatic struct custom_engine *\ncheck_engine(lua_State *L, int index) {\n\tluaL_checktype(L, index, LUA_TLIGHTUSERDATA);\n\treturn (struct custom_engine *)lua_touserdata(L, index);\n}\n\nstatic struct audio_group *\ncheck_group(lua_State *L, int index) {\n\tstruct audio_group *group = (struct audio_group *)luaL_checkudata(L, index, AUDIO_GROUP_METATABLE);\n\tluaL_argcheck(L, group->alive, index, \"closed audio group\");\n\treturn group;\n}\n\nstatic struct audio_sound *\ncheck_sound(lua_State *L, int index) {\n\tstruct audio_sound *sound = (struct audio_sound *)luaL_checkudata(L, index, AUDIO_SOUND_METATABLE);\n\tluaL_argcheck(L, sound->alive, index, \"closed audio sound\");\n\treturn sound;\n}\n\nstatic int\npush_error(lua_State *L, ma_result r) {\n\tlua_pushnil(L);\n\tlua_pushstring(L, ma_result_description(r));\n\treturn 2;\n}\n\nstatic int\nlaudio_group_uninit(lua_State *L) {\n\tstruct audio_group *group = (struct audio_group *)luaL_checkudata(L, 1, AUDIO_GROUP_METATABLE);\n\tif (group->alive) {\n\t\tma_sound_group_uninit(&group->group);\n\t\tgroup->alive = 0;\n\t}\n\treturn 0;\n}\n\nstatic int\nlaudio_sound_uninit(lua_State *L) {\n\tstruct audio_sound *sound = (struct audio_sound *)luaL_checkudata(L, 1, AUDIO_SOUND_METATABLE);\n\tif (sound->alive) {\n\t\tma_sound_uninit(&sound->sound);\n\t\tsound->alive = 0;\n\t}\n\treturn 0;\n}\n\nstatic ma_result\nzr_open(ma_vfs* pVFS, const char* pFilePath, ma_uint32 openMode, ma_vfs_file* pFile) {\n\tstruct custom_vfs *vfs = (struct custom_vfs *)pVFS;\n\tif (openMode != MA_OPEN_MODE_READ)\n\t\treturn MA_NOT_IMPLEMENTED;\n\tzipreader_file zf = zipreader_open(vfs->zipnames, pFilePath);\n\tif (zf == NULL) {\n\t\treturn MA_ERROR;\n\t}\n\t*pFile = (ma_vfs_file)zf;\n\treturn MA_SUCCESS;\n}\n\nstatic ma_result\nzr_close(ma_vfs* pVFS, ma_vfs_file file) {\n\t(void)pVFS;\n\tzipreader_close((zipreader_file)file);\n\treturn MA_SUCCESS;\n}\n\nstatic ma_result\nzr_read(ma_vfs* pVFS, ma_vfs_file file, void* pDst, size_t sizeInBytes, size_t* pBytesRead) {\n\t(void)pVFS;\n\tint bytes = (int)sizeInBytes;\n\tif (bytes!= sizeInBytes || bytes < 0)\n\t\treturn MA_OUT_OF_RANGE;\n\tint rd = zipreader_read((zipreader_file)file, pDst, bytes);\n\tif (rd < 0)\n\t\treturn MA_IO_ERROR;\n\t*pBytesRead = rd;\n\treturn MA_SUCCESS;\n}\n\nstatic ma_result\nzr_seek(ma_vfs* pVFS, ma_vfs_file file, ma_int64 offset, ma_seek_origin origin) {\n\t(void)pVFS;\n\tint whence;\n\tswitch (origin) {\n\tcase ma_seek_origin_start :\n\t\twhence = SEEK_SET;\n\t\tbreak;\n\tcase ma_seek_origin_current :\n\t\twhence = SEEK_CUR;\n\t\tbreak;\n\tcase ma_seek_origin_end :\n\t\twhence = SEEK_END;\n\t\tbreak;\n\tdefault :\n\t\treturn MA_INVALID_ARGS;\n\t}\n\tif (zipreader_seek((zipreader_file)file, offset, whence) != 0) {\n\t\treturn MA_ERROR;\n\t}\n\treturn MA_SUCCESS;\n}\n\nstatic ma_result\nzr_tell(ma_vfs* pVFS, ma_vfs_file file, ma_int64* pCursor) {\n\t(void)pVFS;\n\t*pCursor = zipreader_tell((zipreader_file)file);\n\tif (*pCursor < 0)\n\t\treturn MA_ERROR;\n\treturn MA_SUCCESS;\n}\n\nstatic ma_result\nzr_info(ma_vfs* pVFS, ma_vfs_file file, ma_file_info* pInfo) {\n\t(void)pVFS;\n\tpInfo->sizeInBytes = zipreader_size((zipreader_file)file);\n\treturn MA_SUCCESS;\n}\n\nstatic int\nlaudio_init_vfs(lua_State *L) {\n\tstruct custom_engine *e = (struct custom_engine *)lua_touserdata(L, 1);\n\tluaL_checktype(L, 2, LUA_TUSERDATA);\n\te->vfs.zipnames = lua_touserdata(L, 2);\n\te->vfs.base.cb.onOpen = zr_open;\n\te->vfs.base.cb.onOpenW = NULL;\n\te->vfs.base.cb.onClose = zr_close;\n\te->vfs.base.cb.onRead = zr_read;\n\te->vfs.base.cb.onWrite = NULL;\n\te->vfs.base.cb.onSeek = zr_seek;\n\te->vfs.base.cb.onTell = zr_tell;\n\te->vfs.base.cb.onInfo = zr_info;\n\treturn 0;\n}\n\nstatic int\nlaudio_init(lua_State *L) {\n\tstruct custom_engine *e = (struct custom_engine *)lua_newuserdatauv(L, sizeof(*e), 0);\n\t\n\tma_default_vfs_init(&e->vfs.base, NULL);\n\te->vfs.base.cb.onOpen = vfs_open_local;\n\te->vfs.zipnames = NULL;\n\n    ma_resource_manager_config config = ma_resource_manager_config_init();\n\tconfig.pVFS = &e->vfs;\n\t\n\tma_result r = ma_resource_manager_init(&config, &e->rm);\n\tif (r != MA_SUCCESS) {\n\t\treturn luaL_error(L, \"ma_resource_manager_init() error : %s\", ma_result_description(r));\n\t}\n\t\t\n\tma_engine_config ec = ma_engine_config_init();\n\tec.pResourceManager = &e->rm;\n\tr = ma_engine_init(&ec, &e->engine);\n\tif (r != MA_SUCCESS) {\n\t\treturn luaL_error(L, \"ma_engine_init() error : %s\", ma_result_description(r));\n\t}\n\te->rm.config.decodedFormat = ma_format_f32;\n\te->rm.config.decodedSampleRate = ma_engine_get_sample_rate(&e->engine);\n\tlua_pushlightuserdata(L, (void *)e);\n\t\n\treturn 2;\n}\n\nstatic int\nlaudio_deinit(lua_State *L) {\n\tstruct custom_engine *e = check_engine(L, 1);\n\tma_engine_uninit(&e->engine);\n\tma_resource_manager_uninit(&e->rm);\n\n\treturn 0;\n}\n\nstatic int\nlaudio_group_init(lua_State *L) {\n\tstruct custom_engine *e = check_engine(L, 1);\n\tstruct audio_group *group = (struct audio_group *)lua_newuserdatauv(L, sizeof(*group), 0);\n\tgroup->alive = 0;\n\tma_result r = ma_sound_group_init(&e->engine, 0, NULL, &group->group);\n\tif (r != MA_SUCCESS) {\n\t\tlua_pop(L, 1);\n\t\treturn push_error(L, r);\n\t}\n\tgroup->alive = 1;\n\tluaL_setmetatable(L, AUDIO_GROUP_METATABLE);\n\treturn 1;\n}\n\nstatic int\nlaudio_group_set_volume(lua_State *L) {\n\tstruct audio_group *group = check_group(L, 1);\n\tfloat volume = (float)luaL_checknumber(L, 2);\n\tma_sound_group_set_volume(&group->group, volume);\n\treturn 0;\n}\n\nstatic int\nlaudio_sound_init(lua_State *L) {\n\tstruct custom_engine *e = check_engine(L, 1);\n\tconst char *filename = luaL_checkstring(L, 2);\n\tma_uint32 flags = (ma_uint32)luaL_optinteger(L, 3, 0);\n\tstruct audio_group *group = NULL;\n\tif (!lua_isnoneornil(L, 4)) {\n\t\tgroup = check_group(L, 4);\n\t}\n\n\tstruct audio_sound *sound = (struct audio_sound *)lua_newuserdatauv(L, sizeof(*sound), 0);\n\tsound->alive = 0;\n\tma_result r = ma_sound_init_from_file(&e->engine, filename, flags, group ? &group->group : NULL, NULL, &sound->sound);\n\tif (r != MA_SUCCESS) {\n\t\tlua_pop(L, 1);\n\t\treturn push_error(L, r);\n\t}\n\tsound->alive = 1;\n\tluaL_setmetatable(L, AUDIO_SOUND_METATABLE);\n\treturn 1;\n}\n\nstatic int\nlaudio_sound_start(lua_State *L) {\n\tstruct audio_sound *sound = check_sound(L, 1);\n\tma_result r = ma_sound_start(&sound->sound);\n\tif (r != MA_SUCCESS) {\n\t\treturn push_error(L, r);\n\t}\n\tlua_pushboolean(L, 1);\n\treturn 1;\n}\n\nstatic int\nlaudio_sound_stop(lua_State *L) {\n\tstruct audio_sound *sound = check_sound(L, 1);\n\tma_result r;\n\tif (lua_isnoneornil(L, 2)) {\n\t\tr = ma_sound_stop(&sound->sound);\n\t} else {\n\t\tma_uint64 fade_ms = (ma_uint64)luaL_checkinteger(L, 2);\n\t\tif (fade_ms == 0) {\n\t\t\tr = ma_sound_stop(&sound->sound);\n\t\t} else {\n\t\t\tr = ma_sound_stop_with_fade_in_milliseconds(&sound->sound, fade_ms);\n\t\t}\n\t}\n\tif (r != MA_SUCCESS) {\n\t\treturn push_error(L, r);\n\t}\n\tlua_pushboolean(L, 1);\n\treturn 1;\n}\n\nstatic int\nlaudio_sound_playing(lua_State *L) {\n\tstruct audio_sound *sound = check_sound(L, 1);\n\tlua_pushboolean(L, ma_sound_is_playing(&sound->sound));\n\treturn 1;\n}\n\nstatic int\nlaudio_sound_set_volume(lua_State *L) {\n\tstruct audio_sound *sound = check_sound(L, 1);\n\tfloat volume = (float)luaL_checknumber(L, 2);\n\tma_sound_set_volume(&sound->sound, volume);\n\treturn 0;\n}\n\nstatic int\nlaudio_sound_set_pan(lua_State *L) {\n\tstruct audio_sound *sound = check_sound(L, 1);\n\tfloat pan = (float)luaL_checknumber(L, 2);\n\tma_sound_set_pan(&sound->sound, pan);\n\treturn 0;\n}\n\nstatic int\nlaudio_sound_set_pitch(lua_State *L) {\n\tstruct audio_sound *sound = check_sound(L, 1);\n\tfloat pitch = (float)luaL_checknumber(L, 2);\n\tma_sound_set_pitch(&sound->sound, pitch);\n\treturn 0;\n}\n\nstatic int\nlaudio_sound_set_looping(lua_State *L) {\n\tstruct audio_sound *sound = check_sound(L, 1);\n\tint looping = lua_toboolean(L, 2);\n\tma_sound_set_looping(&sound->sound, looping);\n\treturn 0;\n}\n\nstatic int\nlaudio_sound_seek(lua_State *L) {\n\tstruct audio_sound *sound = check_sound(L, 1);\n\tfloat seconds = (float)luaL_checknumber(L, 2);\n\tma_result r = ma_sound_seek_to_second(&sound->sound, seconds);\n\tif (r != MA_SUCCESS) {\n\t\treturn push_error(L, r);\n\t}\n\tlua_pushboolean(L, 1);\n\treturn 1;\n}\n\nstatic int\nlaudio_sound_tell(lua_State *L) {\n\tstruct audio_sound *sound = check_sound(L, 1);\n\tfloat seconds = 0.0f;\n\tma_result r = ma_sound_get_cursor_in_seconds(&sound->sound, &seconds);\n\tif (r != MA_SUCCESS) {\n\t\treturn push_error(L, r);\n\t}\n\tlua_pushnumber(L, seconds);\n\treturn 1;\n}\n\nint\nluaopen_soluna_audio(lua_State *L) {\n\tluaL_checkversion(L);\n\tif (luaL_newmetatable(L, AUDIO_GROUP_METATABLE)) {\n\t\tlua_pushcfunction(L, laudio_group_uninit);\n\t\tlua_setfield(L, -2, \"__gc\");\n\t}\n\tlua_pop(L, 1);\n\tif (luaL_newmetatable(L, AUDIO_SOUND_METATABLE)) {\n\t\tlua_pushcfunction(L, laudio_sound_uninit);\n\t\tlua_setfield(L, -2, \"__gc\");\n\t}\n\tlua_pop(L, 1);\n\tluaL_Reg l[] = {\n\t\t{ \"init\", laudio_init },\n\t\t{ \"init_vfs\", laudio_init_vfs },\n\t\t{ \"deinit\", laudio_deinit },\n\t\t{ \"group_init\", laudio_group_init },\n\t\t{ \"group_uninit\", laudio_group_uninit },\n\t\t{ \"group_set_volume\", laudio_group_set_volume },\n\t\t{ \"sound_init\", laudio_sound_init },\n\t\t{ \"sound_uninit\", laudio_sound_uninit },\n\t\t{ \"sound_start\", laudio_sound_start },\n\t\t{ \"sound_stop\", laudio_sound_stop },\n\t\t{ \"sound_playing\", laudio_sound_playing },\n\t\t{ \"sound_set_volume\", laudio_sound_set_volume },\n\t\t{ \"sound_set_pan\", laudio_sound_set_pan },\n\t\t{ \"sound_set_pitch\", laudio_sound_set_pitch },\n\t\t{ \"sound_set_looping\", laudio_sound_set_looping },\n\t\t{ \"sound_seek\", laudio_sound_seek },\n\t\t{ \"sound_tell\", laudio_sound_tell },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\treturn 1;\n}\n"
  },
  {
    "path": "src/batch.c",
    "content": "#include \"batch.h\"\n#include <stdlib.h>\n#include <assert.h>\n#include <stdatomic.h>\n\n#define DEFAULT_SIZE 1024\n\nstruct draw_batch {\n\tint cap;\n\tstruct draw_primitive * stream;\n};\n\nstruct draw_batch *\nbatch_new(int size) {\n\tif (size < DEFAULT_SIZE)\n\t\tsize = DEFAULT_SIZE;\n\tstruct draw_batch * batch = (struct draw_batch *)malloc(sizeof(*batch));\n\tif (batch == NULL)\n\t\treturn NULL;\n\tbatch->cap = size;\n\tbatch->stream = (struct draw_primitive *)malloc(sizeof(struct draw_primitive) * size);\n\tif (batch->stream == NULL) {\n\t\tfree(batch);\n\t\treturn NULL;\n\t}\n\treturn batch;\n}\n\nvoid\nbatch_delete(struct draw_batch *B) {\n\tif (B == NULL)\n\t\treturn;\n\tfree(B->stream);\n\tfree(B);\n}\n\nstruct draw_primitive *\nbatch_reserve(struct draw_batch *B, int size) {\n\tif (size <= B->cap)\n\t\treturn B->stream;\n\tint cap = B->cap;\n\tdo {\n\t\tcap = cap * 3/2;\n\t} while (cap < size);\n\tstruct draw_primitive * stream = realloc(B->stream, cap * sizeof(struct draw_primitive));\n\tif (stream == NULL)\n\t\treturn NULL;\n\tB->stream = stream;\n\tB->cap = cap;\n\treturn stream;\n}\n"
  },
  {
    "path": "src/batch.h",
    "content": "#ifndef soluna_batch_h\n#define soluna_batch_h\n\n#include <stdint.h>\n\nstruct draw_primitive {\n\tint32_t x;\t\t// sign bit + 23 + 8   fix number\n\tint32_t y;\n\tuint32_t sr;\t// scale + rot\n\tint32_t sprite;\t\t// negative : material \n};\n\nstruct draw_primitive_external {\n\tint sprite;\n};\n\nstruct draw_batch;\n\nstruct draw_batch * batch_new(int size);\nstruct draw_primitive * batch_reserve(struct draw_batch *, int size);\nvoid batch_delete(struct draw_batch *);\n\n#endif\n"
  },
  {
    "path": "src/blit.glsl",
    "content": "@vs vs\n\nout vec2 uv;\n\nvoid main() {\n\tvec2 position = vec2(gl_VertexIndex & 1, gl_VertexIndex >> 1);\n\tvec2 screen = position * 2.0 - 1.0;\n\tgl_Position = vec4(screen.x, -screen.y, 0.0, 1.0);\n\tuv = position;\n}\n@end\n\n@fs fs\nlayout(binding=0) uniform texture2D tex;\nlayout(binding=0) uniform sampler smp;\n\nin vec2 uv;\nout vec4 frag_color;\n\nvoid main() {\n\tfrag_color = texture(sampler2D(tex, smp), uv);\n}\n@end\n\n@program blit vs fs"
  },
  {
    "path": "src/colorquad.glsl",
    "content": "@vs vs\nlayout(binding=0) uniform vs_params {\n\tvec2 framesize;\n};\n\nstruct sr_mat {\n\tmat2 m;\n};\n\nlayout(binding=0) readonly buffer sr_lut {\n\tsr_mat sr[];\n};\n\nin vec4 position;\nin uint idx;\nin vec4 c;\n\nout vec4 color;\n\nvoid main() {\n\tivec2 u2 = ivec2(0 , position.z);\n\tivec2 v2 = ivec2(0 , position.w);\n\tvec2 uv_offset = vec2(u2[gl_VertexIndex & 1] , v2[gl_VertexIndex >> 1]);\n\tvec2 pos = (uv_offset * sr[idx].m + position.xy) * framesize;\n\tgl_Position = vec4(pos.x - 1.0f, pos.y + 1.0f, 0, 1);\n\tcolor = c;\n}\n\n@end\n\n@fs fs\n\nin vec4 color;\nout vec4 frag_color;\n\nvoid main() {\n\tfrag_color = color;\n}\n@end\n\n@program colorquad vs fs"
  },
  {
    "path": "src/data/settingdefault.dl",
    "content": "sprite_max : 0x40000\ntexture_size : 2048\nsrbuffer_size : 0x10000\nbatch_size : 65536\ndraw_instance : 65536\nentry : main.lua\nproject : soluna\nservice_path : \"./?.lua\"\nwidth : 1024\nheight : 768\nhigh_dpi : false\nwindow_title : soluna\nbackground : 0x4080c0\ntmpbuffer_size : 0x20000\n"
  },
  {
    "path": "src/drawmgr.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n#include <stdint.h>\n\n#include \"batch.h\"\n#include \"spritemgr.h\"\n\nstruct draw_element {\n\tstruct draw_primitive * base;\n\tint n;\n\tint material;\n\tint texture;\n};\n\nstruct drawmgr {\n\tstruct sprite_bank *bank;\n\tint cap;\n\tint n;\n\tint bank_n;\n\tstruct draw_element data[1];\n};\n\nstatic int\nldrawmgr_len(lua_State *L) {\n\tstruct drawmgr * d = lua_touserdata(L, 1);\n\tlua_pushinteger(L, d->n);\n\treturn 1;\n}\n\nstatic int\nldrawmgr_index(lua_State *L) {\n\tstruct drawmgr * d = lua_touserdata(L, 1);\n\tint idx = luaL_checkinteger(L, 2) - 1;\n\tif (idx < 0 || idx >= d->n) {\n\t\treturn 0;\n\t}\n\tstruct draw_element *e = &(d->data[idx]);\n\tlua_pushinteger(L, e->material);\n\tlua_pushlightuserdata(L, e->base);\n\tlua_pushinteger(L, e->n);\n\tif (e->texture >= 0) {\n\t\tlua_pushinteger(L, e->texture);\n\t\treturn 4;\n\t} else {\n\t\treturn 3;\n\t}\n}\n\nstatic int\nldrawmgr_reset(lua_State *L) {\n\tstruct drawmgr * d = (struct drawmgr *)luaL_checkudata(L, 1, \"SOLUNA_DRAWMGR\");\n\td->n = 0;\n\td->bank_n = d->bank->n;\n\treturn 0;\n}\n\nstatic int\nappend_external_material(struct drawmgr * d, struct draw_primitive *base, int n, int matid, int texid) {\n\tint i;\n\tstruct sprite_rect * rect = d->bank->rect;\n\n\tfor (i=1;i<n;i++) {\n\t\tif (base[i*2].sprite != matid) {\n\t\t\tbreak;\n\t\t}\n\t\tstruct draw_primitive_external * ext = (struct draw_primitive_external *)&base[i*2+1];\n\t\tint sprite = ext->sprite;\n\t\tif (sprite >= 0 && rect[sprite].texid != texid) {\n\t\t\tbreak;\n\t\t}\n\t}\n\tstruct draw_element *e = &d->data[d->n++];\n\te->base = base;\n\te->n = i;\n\te->material = -matid;\n\te->texture = texid;\n\treturn i;\n}\n\nstatic int\nappend_default_material(struct drawmgr * d, struct draw_primitive *base, int n, int texid) {\n\tint i;\n\tstruct sprite_rect * rect = d->bank->rect;\n\tint rect_n = d->bank_n;\n\n\tfor (i=1;i<n;i++) {\n\t\tint sprite = base[i].sprite;\n\t\tif (sprite <= 0 || sprite > rect_n)\n\t\t\tbreak;\n\t\t--sprite;\n\t\tif (texid != rect[sprite].texid)\n\t\t\tbreak;\n\t}\n\tstruct draw_element *e = &d->data[d->n++];\n\te->base = base;\n\te->n = i;\n\te->material = 0;\n\te->texture = texid;\n\treturn i;\n}\n\nstatic int\nldrawmgr_append(lua_State *L) {\n\tstruct drawmgr * d = (struct drawmgr *)luaL_checkudata(L, 1, \"SOLUNA_DRAWMGR\");\n\t\n\tstruct draw_primitive *prim = (struct draw_primitive *)lua_touserdata(L, 2);\n\tint prim_n = luaL_checkinteger(L, 3);\n\t\n\tstruct sprite_rect * rect = d->bank->rect;\n\tint rect_n = d->bank_n;\n\n\tint i;\n\tstruct draw_primitive *end_ptr = &prim[prim_n];\n\tfor (i=0;i<prim_n;) {\n\t\tstruct draw_primitive *p = &prim[i];\n\t\tint index = p->sprite;\n\t\tif (d->n >= d->cap) {\n\t\t\treturn luaL_error(L, \"Too many draw\");\n\t\t}\n\t\tif (index <= 0) {\n\t\t\tif (i == prim_n || index == 0) {\n\t\t\t\treturn luaL_error(L, \"Invalid batch stream\");\n\t\t\t}\n\t\t\tstruct draw_primitive_external * ext = (struct draw_primitive_external *)&prim[i+1];\n\t\t\tint sprite = ext->sprite;\n\t\t\tint texid = -1;\n\t\t\tif (sprite >= 0) {\n\t\t\t\ttexid = rect[sprite].texid;\n\t\t\t}\n\t\t\ti += append_external_material(d, p, (end_ptr - p)/2, index, texid) * 2;\n\t\t} else {\n\t\t\t--index;\n\t\t\tif (index >= rect_n)\n\t\t\t\treturn luaL_error(L, \"Invalid sprite id %d\", index);\n\t\t\tint texid = rect[index].texid;\n\t\t\ti += append_default_material(d, p, end_ptr - p, texid);\n\t\t}\n\t}\n\n\treturn 0;\n}\n\nstatic int\nldrawmgr_new(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TLIGHTUSERDATA);\n\tvoid * bank = lua_touserdata(L, 1);\n\tint cap = luaL_checkinteger(L, 2);\n\tstruct drawmgr * d = (struct drawmgr *)lua_newuserdatauv(L, sizeof(*d) + (cap-1)*sizeof(d->data[0]), 0);\n\td->bank = (struct sprite_bank *)bank;\n\td->cap = cap;\n\td->n = 0;\n\tif (luaL_newmetatable(L, \"SOLUNA_DRAWMGR\")) {\n\t\tluaL_Reg l[] = {\n\t\t\t{ \"__index\", NULL },\n\t\t\t{ \"__len\", ldrawmgr_len },\n\t\t\t{ \"__call\", ldrawmgr_index },\n\t\t\t{ \"reset\", ldrawmgr_reset },\n\t\t\t{ \"append\", ldrawmgr_append },\n\t\t\t{ NULL, NULL },\n\t\t};\n\t\tluaL_setfuncs(L, l, 0);\n\n\t\tlua_pushvalue(L, -1);\n\t\tlua_setfield(L, -2, \"__index\");\n\t}\n\tlua_setmetatable(L, -2);\n\t\n\treturn 1;\n}\n\nint\nluaopen_drawmgr(lua_State *L) {\n\tluaL_checkversion(L);\n\tluaL_Reg l[] = {\n\t\t{ \"new\", ldrawmgr_new },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\treturn 1;\n}\n"
  },
  {
    "path": "src/embedlua.c",
    "content": "#include \"bootstrap.lua.h\"\n#include \"service.lua.h\"\n#include \"log.lua.h\"\n#include \"timer.lua.h\"\n#include \"root.lua.h\"\n#include \"main.lua.h\"\n#include \"start.lua.h\"\n#include \"print_r.lua.h\"\n#include \"loader.lua.h\"\n#include \"spritebundle.lua.h\"\n#include \"render.lua.h\"\n#include \"settingdefault.dl.h\"\n#include \"settings.lua.h\"\n#include \"initsetting.lua.h\"\n#include \"fontmgr.lua.h\"\n#include \"gamepad.lua.h\"\n#include \"soluna.lua.h\"\n#include \"icon.lua.h\"\n#include \"layout.lua.h\"\n#include \"text.lua.h\"\n#include \"util.lua.h\"\n#include \"coroutine.lua.h\"\n#include \"packageloader.lua.h\"\n#include \"audio.lua.h\"\n#include \"matdefault.lua.h\"\n#include \"mattext.lua.h\"\n#include \"matquad.lua.h\"\n#include \"matmask.lua.h\"\n\n#include \"lua.h\"\n#include \"lauxlib.h\"\n\n#define REG_SOURCE(name) \\\n\tlua_pushlightuserdata(L, (void *)luasrc_##name);\t\\\n\tlua_pushinteger(L, sizeof(luasrc_##name) - 1);\t\\\n\tlua_pushcclosure(L, get_string, 2);\t\\\n\tlua_setfield(L, -2, #name);\n\n#define REG_MATERIAL(name) \\\n\tlua_pushstring(L, #name);\t\\\n\tlua_rawseti(L, -2, luaL_len(L, -2) + 1);\t\\\n\tREG_SOURCE(name)\n\n#define REG_DATALIST(name) \\\n\tlua_pushlightuserdata(L, (void *)dl_##name);\t\\\n\tlua_pushinteger(L, sizeof(dl_##name) - 1);\t\\\n\tlua_pushcclosure(L, get_stringloader, 2);\t\\\n\tlua_setfield(L, -2, #name);\n\nstatic int\nget_string(lua_State *L) {\n\tconst char * s = (const char *)lua_touserdata(L, lua_upvalueindex(1));\n\tsize_t sz = (size_t)lua_tointeger(L, lua_upvalueindex(2));\n\tlua_pushexternalstring(L, s, sz, NULL, NULL);\n\treturn 1;\n}\n\nstatic int\nget_stringloader(lua_State *L) {\n\tlua_pushvalue(L, lua_upvalueindex(1));\n\tlua_pushvalue(L, lua_upvalueindex(2));\n\treturn 2;\n}\n\nint\nluaopen_embedsource(lua_State *L) {\n\tlua_newtable(L);\n\t\tlua_newtable(L);\t// runtime\n\t\t\tREG_SOURCE(bootstrap)\n\t\t\tREG_SOURCE(service)\n\t\t\tREG_SOURCE(main)\n\t\t\tREG_SOURCE(print_r)\n\t\t\tREG_SOURCE(fontmgr)\n\t\t\tREG_SOURCE(packageloader)\n\t\tlua_setfield(L, -2, \"runtime\");\n\n\t\tlua_newtable(L);\t// runtime\n\t\t\tREG_SOURCE(spritebundle)\n\t\t\tREG_SOURCE(icon)\n\t\t\tREG_SOURCE(layout)\n\t\t\tREG_SOURCE(text)\n\t\t\tREG_SOURCE(soluna)\n\t\t\tREG_SOURCE(util)\n\t\t\tREG_SOURCE(coroutine)\n\t\t\tREG_SOURCE(initsetting)\n\t\tlua_setfield(L, -2, \"lib\");\n\n\t\tlua_newtable(L);\t// service\n\t\t\tREG_SOURCE(log)\n\t\t\tREG_SOURCE(root)\n\t\t\tREG_SOURCE(timer)\n\t\t\tREG_SOURCE(start)\n\t\t\tREG_SOURCE(loader)\n\t\t\tREG_SOURCE(render)\n\t\t\tREG_SOURCE(gamepad)\n\t\t\tREG_SOURCE(settings)\n\t\t\tREG_SOURCE(audio)\n\t\tlua_setfield(L, -2, \"service\");\n\n\t\tlua_newtable(L);\t// material\n\t\t\tREG_MATERIAL(matdefault)\n\t\t\tREG_MATERIAL(mattext)\n\t\t\tREG_MATERIAL(matquad)\n\t\t\tREG_MATERIAL(matmask)\n\t\tlua_setfield(L, -2, \"material\");\n\n\t\tlua_newtable(L);\t// data list\n\t\t\tREG_DATALIST(settingdefault)\n\t\tlua_setfield(L, -2, \"data\");\n\treturn 1;\n}\n"
  },
  {
    "path": "src/entry.c",
    "content": "#define SOKOL_IMPL\n\n#include <lua.h>\n#include <lauxlib.h>\n#include <string.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <stdint.h>\n#include <stdbool.h>\n#include <locale.h>\n\n#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)\n\n#define SOKOL_D3D11\n\n#elif defined(__APPLE__)\n\n#define SOKOL_METAL\n\n#elif defined(__EMSCRIPTEN__)\n\n#define SOKOL_WGPU\n\n#elif defined(__linux__)\n\n#define SOKOL_GLCORE\n\n#else\n\n#error Unsupport platform\n\n#endif\n\n#include \"version.h\"\n\n#define FRAME_CALLBACK 1\n#define CLEANUP_CALLBACK 2\n#define EVENT_CALLBACK 3\n#define CALLBACK_COUNT 3\n\n#include \"sokol/sokol_app.h\"\n#include \"sokol/sokol_gfx.h\"\n#include \"sokol/sokol_glue.h\"\n#include \"sokol/sokol_log.h\"\n#include \"sokol/sokol_args.h\"\n#include \"loginfo.h\"\n#include \"appevent.h\"\n#include \"ime_state.h\"\n\n#if defined(__APPLE__)\n#include \"platform/macos/soluna_macos_ime.h\"\n#endif\n#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)\n#include \"platform/windows/soluna_windows_ime.h\"\n#endif\n#if defined(__linux__)\n#include \"platform/linux/soluna_linux_ime.h\"\n#endif\n#if defined(__EMSCRIPTEN__)\n#include \"platform/wasm/soluna_wasm_ime.h\"\n#endif\n\n\n\nstatic void app_event(const sapp_event* ev);\n\n#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)\n\n#define PLATFORM \"windows\"\n\n#elif defined(__APPLE__)\n\n#define PLATFORM \"macos\"\n\n#elif defined(__EMSCRIPTEN__)\n\n#define PLATFORM \"wasm\"\n\n#elif defined(__linux__)\n\n#define PLATFORM \"linux\"\n\n#else\n\n#define PLATFORM \"unknown\"\n\n#endif\n\nstruct app_context {\n\tlua_State *L;\n\tlua_State *quitL;\n\tint (*send_log)(void *ud, unsigned int id, void *data, uint32_t sz);\n\tvoid *send_log_ud;\n\tvoid *mqueue;\n};\n\nstatic struct app_context *CTX = NULL;\n\nstruct soluna_ime_rect_state g_soluna_ime_rect = { 0.0f, 0.0f, 0.0f, 0.0f, 0, false };\n\nvoid soluna_emit_char(uint32_t codepoint, uint32_t modifiers, bool repeat);\n\nstruct soluna_message {\n\tconst char *type;\n\tunion {\n\t\tint p[2];\n\t\tuint64_t u64;\n\t} v;\n};\n\nstatic inline struct soluna_message *\nmessage_create(const char *type, int p1, int p2) {\n\tstruct soluna_message *msg = (struct soluna_message *)malloc(sizeof(*msg));\n\tmsg->type = type;\n\tmsg->v.p[0] = p1;\n\tmsg->v.p[1] = p2;\n\treturn msg;\n}\n\nstatic inline struct soluna_message *\nmessage_create64(const char *type, uint64_t p) {\n\tstruct soluna_message *msg = (struct soluna_message *)malloc(sizeof(*msg));\n\tmsg->type = type;\n\tmsg->v.u64 = p;\n\treturn msg;\n}\n\nstatic inline void\nmessage_release(struct soluna_message *msg) {\n\tfree(msg);\n}\n\nvoid\nsoluna_emit_char(uint32_t codepoint, uint32_t modifiers, bool repeat) {\n\tsapp_event ev;\n\tmemset(&ev, 0, sizeof(ev));\n\tev.type = SAPP_EVENTTYPE_CHAR;\n\tev.frame_count = sapp_frame_count();\n\tev.char_code = codepoint;\n\tev.modifiers = modifiers;\n\tev.key_repeat = repeat;\n\tapp_event(&ev);\n}\n\nstatic int\nlmessage_send(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TLIGHTUSERDATA);\n\tluaL_checktype(L, 2, LUA_TLIGHTUSERDATA);\n\tint (*send_message)(void *ud, void *p) = lua_touserdata(L, 1);\n\tvoid *send_message_ud = lua_touserdata(L, 2);\n\tconst char * what = NULL;\n\tif (lua_type(L, 3) == LUA_TSTRING) {\n\t\twhat = lua_tostring(L, 3);\n\t} else {\n\t\tluaL_checktype(L, 3, LUA_TLIGHTUSERDATA);\n\t\twhat = (const char *)lua_touserdata(L, 3);\n\t}\n\tint64_t p1 = luaL_optinteger(L, 4, 0);\n\tstruct soluna_message * msg = NULL;\n\tif (lua_isnoneornil(L, 5)) {\n\t\tmsg = message_create64(what, p1);\n\t} else {\n\t\tint p2 = luaL_checkinteger(L, 5);\n\t\tmsg = message_create(what, (int)p1, p2);\n\t}\n\tint fail = send_message(send_message_ud, msg);\n\tif (fail) {\n\t\tmessage_release(msg);\n\t}\n\tlua_pushboolean(L, !fail);\n\treturn 1;\n}\n\nstatic int\nlmessage_unpack(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TLIGHTUSERDATA);\n\tstruct soluna_message *m = (struct soluna_message *)lua_touserdata(L,1);\n\tlua_pushstring(L, m->type);\n\tlua_pushinteger(L, m->v.p[0]);\n\tlua_pushinteger(L, m->v.p[1]);\n\tlua_pushinteger(L, m->v.u64);\n\tmessage_release(m);\n\treturn 4;\n}\n\nstatic void\nrequest_app_quit(void) {\n\tif (CTX) {\n\t\tCTX->quitL = CTX->L;\n\t\tCTX->L = NULL;\n\t}\n}\n\nstatic int\nlquit_signal(lua_State *L) {\n\trequest_app_quit();\n\treturn 0;\n}\n\n#if defined(__EMSCRIPTEN__)\nEMSCRIPTEN_KEEPALIVE\nvoid\nsoluna_runtime_quit(void) {\n\trequest_app_quit();\n}\n#endif\n\nstatic int\nlevent_unpack(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TLIGHTUSERDATA);\n\tstruct event_message em;\n\tapp_event_unpack(&em, lua_touserdata(L, 1));\n\tlua_pushlightuserdata(L, (void *)em.typestr);\n\tlua_pushinteger(L, em.p1);\n\tlua_pushinteger(L, em.p2);\n\tlua_pushinteger(L, em.p3);\n\treturn 4;\n}\n\nstatic int\nlset_window_title(lua_State *L) {\n\tif (CTX == NULL || lua_type(L, 1) != LUA_TSTRING)\n\t\treturn 0;\n\tconst char * text = lua_tostring(L, 1);\n\tsapp_set_window_title(text);\n\treturn 0;\n}\n\nstruct icon_pixels {\n\tuint8_t *ptr;\n};\n\nstatic void\nicon_free_pixels(struct icon_pixels *payloads, int count) {\n\tfor (int i = 0; i < count; ++i) {\n\t\tfree(payloads[i].ptr);\n\t}\n}\n\nstatic int\nicon_get_int(lua_State *L, int index, const char *field, const char *fallback) {\n\tint abs_index = lua_absindex(L, index);\n\tint value = 0;\n\tint type = lua_getfield(L, abs_index, field);\n\tif (type == LUA_TNUMBER) {\n\t\tvalue = (int)lua_tointeger(L, -1);\n\t\tlua_pop(L, 1);\n\t\treturn value;\n\t}\n\tlua_pop(L, 1);\n\tif (fallback) {\n\t\ttype = lua_getfield(L, abs_index, fallback);\n\t\tif (type == LUA_TNUMBER) {\n\t\t\tvalue = (int)lua_tointeger(L, -1);\n\t\t\tlua_pop(L, 1);\n\t\t\treturn value;\n\t\t}\n\t\tlua_pop(L, 1);\n\t}\n\tluaL_error(L, \"icon missing %s\", field);\n\treturn 0;\n}\n\nstatic void\nicon_copy_image(lua_State *L, int index, sapp_image_desc *dst, struct icon_pixels *payload) {\n\tint abs_index = lua_absindex(L, index);\n\tint width = icon_get_int(L, abs_index, \"w\", \"width\");\n\tint height = icon_get_int(L, abs_index, \"h\", \"height\");\n\tif (width <= 0 || height <= 0) {\n\t\tluaL_error(L, \"icon size (%d * %d) must be positive\", width, height);\n\t}\n\n\tsize_t stride = 0;\n\tif (lua_getfield(L, abs_index, \"stride\") == LUA_TNUMBER) {\n\t\tstride = (size_t)lua_tointeger(L, -1);\n\t}\n\tlua_pop(L, 1);\n\n\tsize_t explicit_size = 0;\n\tif (lua_getfield(L, abs_index, \"size\") == LUA_TNUMBER) {\n\t\texplicit_size = (size_t)lua_tointeger(L, -1);\n\t}\n\tlua_pop(L, 1);\n\n\tint type = lua_getfield(L, abs_index, \"data\");\n\tconst uint8_t *src = NULL;\n\tsize_t src_size = 0;\n\tif (type == LUA_TSTRING) {\n\t\tsrc = (const uint8_t *)lua_tolstring(L, -1, &src_size);\n\t} else if (type == LUA_TUSERDATA) {\n\t\tsrc = (const uint8_t *)lua_touserdata(L, -1);\n\t\tsrc_size = lua_rawlen(L, -1);\n\t} else if (type == LUA_TLIGHTUSERDATA) {\n\t\tsrc = (const uint8_t *)lua_touserdata(L, -1);\n\t\tsrc_size = explicit_size;\n\t} else {\n\t\tlua_pop(L, 1);\n\t\tluaL_error(L, \"icon.data must be buffer\");\n\t}\n\tlua_pop(L, 1);\n\tif (src == NULL) {\n\t\tluaL_error(L, \"icon data missing\");\n\t}\n\n\tsize_t row_bytes = (size_t)width * 4;\n\tif (stride == 0) {\n\t\tstride = row_bytes;\n\t}\n\tif (stride < row_bytes) {\n\t\tluaL_error(L, \"icon stride < width\");\n\t}\n\tif (!(type == LUA_TLIGHTUSERDATA && explicit_size == 0)) {\n\t\tsize_t required = stride * (size_t)height;\n\t\tif (src_size < required) {\n\t\t\tluaL_error(L, \"icon buffer too small\");\n\t\t}\n\t}\n\n\tsize_t copy_size = row_bytes * (size_t)height;\n\tuint8_t *copy = (uint8_t *)malloc(copy_size);\n\tif (copy == NULL) {\n\t\tluaL_error(L, \"icon alloc fail\");\n\t}\n\n\tif (stride == row_bytes) {\n\t\tmemcpy(copy, src, copy_size);\n\t} else {\n\t\tconst uint8_t *s = src;\n\t\tuint8_t *d = copy;\n\t\tint y;\n\t\tfor (y = 0; y < height; ++y) {\n\t\t\tmemcpy(d, s, row_bytes);\n\t\t\ts += stride;\n\t\t\td += row_bytes;\n\t\t}\n\t}\n\n\tdst->width = width;\n\tdst->height = height;\n\tdst->pixels.ptr = copy;\n\tdst->pixels.size = copy_size;\n\tpayload->ptr = copy;\n}\n\nstatic float\nget_field_float(lua_State *L, int index, const char *field) {\n\tlua_getfield(L, index, field);\n\tif (lua_getfield(L, index, field) != LUA_TNUMBER) {\n      return luaL_error(L, \"Invalid .%s type (%s is not a number)\",\n        field, lua_typename(L, lua_type(L, -1)));\n\t}\n \tfloat value = (float)lua_tonumber(L, -1);\n\tlua_pop(L, 1);\n\treturn value;\n}\n\nstatic int\nlset_icon(lua_State *L) {\n\tif (lua_isnoneornil(L, 1))\n\t\treturn 0;\n\n\tluaL_checktype(L, 1, LUA_TTABLE);\n\n\tsapp_icon_desc desc;\n\tmemset(&desc, 0, sizeof(desc));\n\tstruct icon_pixels payloads[SAPP_MAX_ICONIMAGES];\n\tmemset(payloads, 0, sizeof(payloads));\n\tint count = 0;\n\tint abs_index = lua_absindex(L, 1);\n\n\tif (lua_getfield(L, abs_index, \"data\") != LUA_TNIL) {\n\t\tlua_pop(L, 1);\n\t\ticon_copy_image(L, abs_index, &desc.images[count], &payloads[count]);\n\t\t++count;\n\t} else {\n\t\tlua_pop(L, 1);\n\t\tint len = (int)lua_rawlen(L, abs_index);\n\t\tfor (int i = 1; i <= len; ++i) {\n\t\t\tif (count >= SAPP_MAX_ICONIMAGES) {\n\t\t\t\ticon_free_pixels(payloads, count);\n\t\t\t\tluaL_error(L, \"too many icon images\");\n\t\t\t}\n\t\t\tlua_rawgeti(L, abs_index, i);\n\t\t\tluaL_checktype(L, -1, LUA_TTABLE);\n\t\t\ticon_copy_image(L, -1, &desc.images[count], &payloads[count]);\n\t\t\t++count;\n\t\t\tlua_pop(L, 1);\n\t\t}\n\t}\n\n\tif (count > 0) {\n\t\tsapp_set_icon(&desc);\n\t}\n\n\ticon_free_pixels(payloads, count);\n\treturn 0;\n}\n\nstatic int\nlset_ime_rect(lua_State *L) {\n#if defined(__APPLE__) || defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__) || defined(__linux__) || defined(__EMSCRIPTEN__)\n\tif (lua_isnoneornil(L, 1)) {\n\t\tg_soluna_ime_rect.text_color = 0;\n\t\tg_soluna_ime_rect.valid = false;\n#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)\n\t\tsoluna_win32_apply_ime_rect();\n#elif defined(__APPLE__)\n\t\tsoluna_macos_hide_ime_label();\n#elif defined(__linux__)\n\t\tsoluna_linux_on_rect_cleared();\n#elif defined(__EMSCRIPTEN__)\n\t\tsoluna_wasm_hide();\n#endif\n\t\treturn 0;\n\t}\n\tluaL_checktype(L, 1, LUA_TTABLE);\n\tg_soluna_ime_rect.x = get_field_float(L, 1, \"x\");\n\tg_soluna_ime_rect.y = get_field_float(L, 1, \"y\");\n\tg_soluna_ime_rect.w = get_field_float(L, 1, \"width\");\n\tg_soluna_ime_rect.h = get_field_float(L, 1, \"height\");\n\n\tif (lua_getfield(L, 1, \"text_color\") == LUA_TNIL) {\n\t\tg_soluna_ime_rect.text_color = 0;\n\t} else {\n\t\tuint32_t color = (uint32_t)luaL_checkinteger(L, -1);\n\t\tif ((color & 0xff000000) == 0) {\n\t\t\tcolor |= 0xff000000;\n\t\t}\n\t\tg_soluna_ime_rect.text_color = color;\n\t}\n\tlua_pop(L, 1);\n\tg_soluna_ime_rect.valid = true;\n#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)\n\tsoluna_win32_apply_ime_rect();\n#elif defined(__APPLE__)\n\tsoluna_macos_apply_ime_rect();\n#elif defined(__linux__)\n\tsoluna_linux_update_spot();\n#elif defined(__EMSCRIPTEN__)\n\tsoluna_wasm_apply_rect();\n#endif\n#endif\n\treturn 0;\n}\n\nstatic int\nlset_ime_font(lua_State *L) {\n\tconst char *name = NULL;\n\tfloat size = 0.0f;\n\tint top = lua_gettop(L);\n\tif (top == 0) {\n#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)\n\t\tsoluna_win32_reset_ime_font();\n#endif\n#if defined(__APPLE__)\n\t\tsoluna_macos_set_ime_font(NULL, 0.0f);\n#endif\n#if defined(__EMSCRIPTEN__)\n\t\tsoluna_wasm_set_font(NULL, 0.0f);\n#endif\n\t\treturn 0;\n\t}\n\tif (top == 1) {\n\t\tsize = (float)luaL_checknumber(L, 1);\n\t} else {\n\t\tif (!lua_isnoneornil(L, 1)) {\n\t\t\tif (lua_type(L, 1) != LUA_TSTRING) {\n\t\t\t\treturn luaL_error(L, \"set_ime_font expects string font name\");\n\t\t\t}\n\t\t\tname = lua_tostring(L, 1);\n\t\t}\n\t\tsize = (float)luaL_checknumber(L, 2);\n\t}\n\tif (size < 0.0f) {\n\t\tsize = 0.0f;\n\t}\n#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)\n\tsoluna_win32_set_ime_font(name, size);\n#endif\n#if defined(__APPLE__)\n\tsoluna_macos_set_ime_font(name, size);\n#endif\n#if defined(__EMSCRIPTEN__)\n\tsoluna_wasm_set_font(name, size);\n#endif\n\treturn 0;\n}\n\nstatic int\nlclose_window(lua_State *L) {\n\tsapp_quit();\n\treturn 0;\n}\n\nstatic int\nlmqueue(lua_State *L) {\n\tif (CTX == NULL || CTX->mqueue == NULL) {\n\t\treturn luaL_error(L, \"Not init mqueue\");\n\t}\n\tlua_pushlightuserdata(L, CTX->mqueue);\n\treturn 1;\n}\n\nstatic int\nlversion(lua_State *L) {\n\tlua_pushinteger(L, SOLUNA_API_VERSION);\n\tlua_pushstring(L, SOLUNA_HASH_VERSION);\n\treturn 2;\n}\n\nstatic void\ndesc_get_boolean(lua_State *L, bool *r, int index, const char * key) {\n\tif (lua_getfield(L, index, key) == LUA_TBOOLEAN) {\n\t\t*r = lua_toboolean(L, -1);\n\t} else {\n\t\tluaL_checktype(L, -1, LUA_TNIL);\n\t}\n\tlua_pop(L, 1);\n}\n\nstatic void\ndesc_get_int(lua_State *L, int *r, int index, const char * key) {\n\tif (lua_getfield(L, index, key) == LUA_TNUMBER) {\n\t\t*r = lua_tointeger(L, -1);\n\t} else {\n\t\tluaL_checktype(L, -1, LUA_TNIL);\n\t}\n\tlua_pop(L, 1);\n}\n\nstatic void\ndesc_get_string(lua_State *L, const char **r, int index, const char * key) {\n\tif (lua_getfield(L, index, key) == LUA_TSTRING) {\n\t\t*r = lua_tostring(L, -1);\n\t} else {\n\t\tluaL_checktype(L, -1, LUA_TNIL);\n\t}\n\tlua_pop(L, 1);\n}\n\nstatic int\nlinit_desc(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TLIGHTUSERDATA);\n\tluaL_checktype(L, 2, LUA_TTABLE);\n\tsapp_desc *d = lua_touserdata(L, 1);\n\tdesc_get_boolean(L, &d->high_dpi, 2, \"high_dpi\");\n\tdesc_get_boolean(L, &d->fullscreen, 2, \"fullscreen\");\n\tdesc_get_int(L, &d->width, 2, \"width\");\n\tdesc_get_int(L, &d->height, 2, \"height\");\n\tdesc_get_string(L, &d->window_title, 2, \"window_title\");\n\n\treturn 0;\n}\n\nint\nluaopen_soluna_app(lua_State *L) {\n\tluaL_checkversion(L);\n\tluaL_Reg l[] = {\n\t\t{ \"mqueue\", lmqueue },\n\t\t{ \"unpackmessage\", lmessage_unpack },\n\t\t{ \"sendmessage\", lmessage_send },\n\t\t{ \"unpackevent\", levent_unpack },\n\t\t{ \"set_window_title\", lset_window_title },\n\t\t{ \"set_icon\", lset_icon },\n\t\t{ \"set_ime_rect\", lset_ime_rect },\n\t\t{ \"set_ime_font\", lset_ime_font },\n\t\t{ \"quit\", lquit_signal },\n\t\t{ \"close_window\", lclose_window },\n\t\t{ \"platform\", NULL },\n\t\t{ \"version\", lversion },\n\t\t{ \"init_desc\", linit_desc },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\tlua_pushliteral(L, PLATFORM);\n\tlua_setfield(L, -2, \"platform\");\n\n\treturn 1;\n}\n\nstatic void\nlog_func(const char* tag, uint32_t log_level, uint32_t log_item, const char* message, uint32_t line_nr, const char* filename, void* user_data) {\n\tif (CTX == NULL || CTX->send_log == NULL) {\n\t\tfprintf(stderr, \"%s (%d) : %s\\n\", filename, line_nr, message);\n\t\treturn;\n\t}\n\tstruct log_info *msg = (struct log_info *)malloc(sizeof(*msg));\n\tif (tag) {\n\t\tstrncpy(msg->tag, tag, sizeof(msg->tag));\n\t\tmsg->tag[sizeof(msg->tag)-1] = 0;\n\t} else {\n\t\tmsg->tag[0] = 0;\n\t}\n\tmsg->log_level = log_level;\n\tmsg->log_item = log_item;\n\tmsg->line_nr = line_nr;\n\tif (message) {\n\t\tstrncpy(msg->message, message, sizeof(msg->message));\n\t\tmsg->message[sizeof(msg->message)-1] = 0;\n\t} else {\n\t\tmsg->message[0] = 0;\n\t}\n\tmsg->filename = filename;\n\tCTX->send_log(CTX->send_log_ud, 0, msg, sizeof(*msg));\n}\n\nvoid soluna_openlibs(lua_State *L);\n\nstatic const char *code = \"local embed = require 'soluna.embedsource' ; local f = load(embed.runtime.main()) ; return f(...)\";\n\nstatic int\npmain(lua_State *L) {\n\tchar** argv = (char **)lua_touserdata(L, 1);\n\tsoluna_openlibs(L);\n\tint n = sargs_num_args();\n\tluaL_checkstack(L, n+1, NULL);\n\tint i;\n\tlua_newtable(L);\n\tint arg_table = lua_gettop(L);\n\tfor (i=0;i<n;i++) {\n\t\tconst char *k = sargs_key_at(i);\n\t\tconst char *v = sargs_value_at(i);\n\t\tif (v[0] == 0) {\n\t\t\tlua_pushstring(L, argv[i+1]);\n\t\t} else {\n\t\t\tlua_pushstring(L, v);\n\t\t\tlua_setfield(L, arg_table, k);\n\t\t}\n\t}\n\tint arg_n = lua_gettop(L) - arg_table + 1;\n\tif (luaL_loadstring(L, code) != LUA_OK) {\n\t\treturn lua_error(L);\n\t}\n\tlua_insert(L, -arg_n-1);\n\tlua_call(L, arg_n, 1);\n\treturn 1;\n}\n\nstatic void *\nget_ud(lua_State *L, const char *key) {\n\tif (lua_getfield(L, -1, key) != LUA_TLIGHTUSERDATA) {\n\t\tlua_pop(L, 1);\n\t\treturn NULL;\n\t}\n\tvoid * ud = lua_touserdata(L, -1);\n\tlua_pop(L, 1);\n\treturn ud;\n}\n\nstatic int\nget_function(lua_State *L, const char *key, int index) {\n\tif (lua_getfield(L, -1, key) != LUA_TFUNCTION) {\n\t\tfprintf(stderr, \"main.lua need return a %s function\", key);\n\t\treturn 1;\n\t}\n\tlua_insert(L, index);\n\treturn 0;\n}\n\nstatic int\ninit_callback(lua_State *L, struct app_context * ctx) {\n\tif (!lua_istable(L, -1)) {\n\t\tfprintf(stderr, \"main.lua need return a table, it's %s\\n\", lua_typename(L, lua_type(L, -1)));\n\t\treturn 1;\n\t}\n\tctx->send_log = get_ud(L, \"send_log\");\n\tctx->send_log_ud = get_ud(L, \"send_log_ud\");\n\tctx->mqueue = get_ud(L, \"mqueue\");\n\tif (get_function(L, \"frame\", FRAME_CALLBACK))\n\t\treturn 1;\n\tif (get_function(L, \"cleanup\", CLEANUP_CALLBACK))\n\t\treturn 1;\n\tif (get_function(L, \"event\", EVENT_CALLBACK))\n\t\treturn 1;\n\tlua_settop(L, CALLBACK_COUNT);\n\treturn 0;\n}\n\nstatic int\nmsghandler(lua_State *L) {\n\tconst char *msg = lua_tostring(L, 1);\n\tluaL_traceback(L, L, msg, 1);\n\treturn 1;\n}\n\nstatic void\nget_app_info(lua_State *L) {\n\tlua_newtable(L);\n\tconst float dpi_scale = sapp_dpi_scale();\n\tconst float safe_scale = dpi_scale > 0.0f ? dpi_scale : 1.0f;\n\tconst int fb_width = sapp_width();\n\tconst int fb_height = sapp_height();\n\tconst int logical_width = (int)((float)fb_width / safe_scale + 0.5f);\n\tconst int logical_height = (int)((float)fb_height / safe_scale + 0.5f);\n\tlua_pushinteger(L, logical_width);\n\tlua_setfield(L, -2, \"width\");\n\tlua_pushinteger(L, logical_height);\n\tlua_setfield(L, -2, \"height\");\n}\n\nstatic int\nstart_app(lua_State *L) {\n\tif (L == NULL) {\n\t\tfprintf(stderr, \"Can't open lua state\\n\");\n\t\treturn 1;\n\t}\n\n\tif (lua_islightuserdata(L, 1)) {\n\t\tfprintf(stderr, \"Init fatal : %s\\n\", (const char *)lua_touserdata(L, 1));\n\t\treturn 1;\n\t}\n\n\tif (lua_gettop(L) != 2) {\n\t\tfprintf(stderr, \"Invalid lua stack (top = %d)\\n\", lua_gettop(L));\n\t\treturn 1;\n\t}\n\tif (lua_getfield(L, -1, \"start\") != LUA_TFUNCTION) {\n\t\tfprintf(stderr, \"No start function\\n\");\n\t\treturn 1;\n\t}\n\tlua_replace(L, -2);\n\tget_app_info(L);\n\tif (lua_pcall(L, 1, 1, 1) != LUA_OK) {\n\t\tfprintf(stderr, \"Start fatal : %s\\n\", lua_tostring(L, -1));\n\t\treturn 1;\n\t} else {\n\t\treturn init_callback(L, CTX);\n\t}\n}\n\nstatic void\napp_init() {\n#if defined(__APPLE__)\n\tsoluna_macos_install_ime();\n#endif\n#if defined(__linux__)\n\tsoluna_linux_ensure_im();\n#endif\n#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)\n\tsoluna_win32_install_wndproc();\n#endif\n\tsg_setup(&(sg_desc) {\n        .environment = sglue_environment(),\n        .logger.func = log_func,\t\t\t\n\t});\n\t\t\n\tlua_State *L = CTX->L;\n\tif (start_app(L)) {\n\t\tif (L) {\n\t\t\tlua_close(L);\n\t\t}\n\t\tCTX->L = NULL;\n\t\tCTX->quitL = NULL;\n\t\tsapp_quit();\n\t}\n}\n\nstatic lua_State *\nget_L(struct app_context *ctx) {\n\tif (ctx == NULL)\n\t\treturn NULL;\n\tlua_State *L = ctx->L;\n\tif (L == NULL) {\n\t\tif (ctx->quitL != NULL) {\n\t\t\tsapp_quit();\n\t\t}\n\t}\n\treturn L;\n}\n\nstatic void\ninvoke_callback(lua_State *L, int index, int nargs) {\n\tlua_pushvalue(L, index);\n\tif (nargs > 0) {\n\t\tlua_insert(L, -nargs-1);\n\t}\n\tif (lua_pcall(L, nargs, 0, 0) != LUA_OK) {\n\t\tfprintf(stderr, \"Error : %s\\n\", lua_tostring(L, -1));\n\t\tlua_pop(L, 1);\n\t}\n}\n\nstatic void\napp_frame() {\n\tlua_State *L = get_L(CTX);\n\tif (L) {\n\t\tlua_pushinteger(L, sapp_frame_count());\n\t\tinvoke_callback(L, FRAME_CALLBACK, 1);\n\t}\n}\n\nstatic void\napp_cleanup() {\n\tif (CTX == NULL)\n\t\treturn;\n\tlua_State *L = CTX->quitL;\n\tif (L == NULL) {\n\t\tL = CTX->L;\n\t\tCTX->L = NULL;\n\t}\n\tif (L) {\n\t\tinvoke_callback(L, CLEANUP_CALLBACK, 0);\n\t\tlua_close(L);\n\t\tCTX->quitL = NULL;\n\t}\n#if defined(__linux__)\n\tsoluna_linux_shutdown_ime();\n#endif\n#if defined(__EMSCRIPTEN__)\n\tsoluna_wasm_hide();\n#endif\n\tsg_shutdown();\n}\n\nstatic void\napp_event(const sapp_event* ev) {\n#if defined(__APPLE__)\n\tif (soluna_macos_is_composition_active() &&\n\t\t(ev->type == SAPP_EVENTTYPE_KEY_DOWN ||\n\t\t ev->type == SAPP_EVENTTYPE_KEY_UP)) {\n\t\treturn;\n\t}\n#endif\n#if defined(__EMSCRIPTEN__)\n\tif (soluna_wasm_should_block_key_event(ev)) {\n\t\treturn;\n\t}\n#endif\n#if defined(__linux__)\n\tif (soluna_linux_should_skip_event(ev)) {\n\t\treturn;\n\t}\n#endif\n#if defined(__EMSCRIPTEN__)\n\tif (soluna_wasm_filter_char_event(ev)) {\n\t\treturn;\n\t}\n#endif\n#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)\n\tif (ev->type == SAPP_EVENTTYPE_FOCUSED && g_soluna_ime_rect.valid) {\n\t\tsoluna_win32_apply_ime_rect();\n\t}\n#endif\n#if defined(__linux__)\n\tsoluna_linux_handle_event(ev);\n#endif\n#if defined(__EMSCRIPTEN__)\n\tsoluna_wasm_handle_event(ev);\n#endif\n\tlua_State *L = get_L(CTX);\n\tif (L) {\n\t\tlua_pushlightuserdata(L, (void *)ev);\n\t\tinvoke_callback(L, EVENT_CALLBACK, 1);\n\t}\n}\n\nstatic int\ninit_settings(lua_State *L, sapp_desc *desc) {\n\tif (lua_gettop(L) != 2) {\n\t\tlua_pushlightuserdata(L, (void *)\"Invalid lua stack\");\n\t\treturn 1;\n\t}\n\tif (lua_getfield(L, -1, \"init\") != LUA_TFUNCTION) {\n\t\tlua_pushlightuserdata(L, (void *)\"No start function\");\n\t\treturn 1;\n\t}\n\tlua_pushlightuserdata(L, (void *)desc);\n\tif (lua_pcall(L, 1, 0, 1) != LUA_OK) {\n\t\tconst char * err = lua_tostring(L, -1);\n\t\tlua_pushlightuserdata(L, (void *)err);\n\t\treturn 1;\n\t} else {\n\t\treturn 0;\n\t}\n}\n\nsapp_desc\nsokol_main(int argc, char* argv[]) {\n\t// init sargs\n\tsargs_desc arg_desc;\n\tmemset(&arg_desc, 0, sizeof(arg_desc));\n\targ_desc.argc = argc;\n\targ_desc.argv = argv;\n\tsargs_setup(&arg_desc);\n\t\n\t// default sapp_desc\n\tsapp_desc d;\n\tmemset(&d, 0, sizeof(d));\n\n\td.init_cb = app_init;\n\td.frame_cb = app_frame;\n\td.cleanup_cb = app_cleanup;\n\td.event_cb = app_event;\n\td.logger.func = log_func;\n\td.win32.console_utf8 = 1;\n\td.win32.console_attach = 1;\n\td.alpha = 0;\n\t\n\t// init L\n\tstatic struct app_context app;\n\tlua_State *L = luaL_newstate();\n\n\tif (L) {\n\t\tlua_settop(L, 0);\n\t\tlua_pushcfunction(L, msghandler);\n\t\tlua_pushcfunction(L, pmain);\n\t\tlua_pushlightuserdata(L, (void *)argv);\n\t\t\n\t\tif (lua_pcall(L, 1, 1, 1) != LUA_OK) {\n\t\t\tconst char * err = lua_tostring(L, -1);\n\t\t\tlua_pushlightuserdata(L, (void *)err);\n\t\t\tlua_replace(L, 1);\n\t\t}\n\t\tsargs_shutdown();\n\t\t\n\t\tif (init_settings(L, &d)) {\n\t\t\tlua_replace(L, 1);\n\t\t}\n\t}\n\n\tapp.L = L;\n\tapp.quitL = NULL;\n\tapp.send_log = NULL;\n\tapp.send_log_ud = NULL;\n\tapp.mqueue = NULL;\n\t\n\tCTX = &app;\n\n\treturn d;\n}\n"
  },
  {
    "path": "src/extapi.c",
    "content": "#include <assert.h>\n#include <stddef.h>\n#include <stdint.h>\n#include <stdlib.h>\n#include <string.h>\n\n#include \"sokol/sokol_gfx.h\"\n#include \"batch.h\"\n#include \"spritemgr.h\"\n#include \"render_bindings.h\"\n#include \"extapi_types.h\"\n\n#define STREAM_FIX_SCALE 256.0f\n#define STREAM_FIX_INV_SCALE (1.0f / STREAM_FIX_SCALE)\n#define STREAM_ERROR_SIZE 128\n\n#if defined(_MSC_VER)\n#define SOLUNA_THREAD_LOCAL __declspec(thread)\n#elif defined(__GNUC__)\n#define SOLUNA_THREAD_LOCAL __thread\n#else\n#define SOLUNA_THREAD_LOCAL _Thread_local\n#endif\n\ntypedef void (*material_submit_stride_func)(void *ud, struct soluna_material_stream_context ctx, int n);\n\nstruct material_stream_context_impl {\n\tconst char *data;\n\tint n;\n\tint material_id;\n\tsoluna_material_error error;\n\tchar error_buffer[STREAM_ERROR_SIZE];\n};\n\nstatic SOLUNA_THREAD_LOCAL char submit_error_buffer[STREAM_ERROR_SIZE];\n\nstatic struct material_stream_context_impl *\nstream_context(struct soluna_material_stream_context ctx) {\n\treturn (struct material_stream_context_impl *)ctx.ctx;\n}\n\nstatic soluna_material_error\ncopy_error(char *buffer, size_t size, const char *error) {\n\tconst char *message = error != NULL ? error : \"Material stream error\";\n\tsize_t len = strlen(message);\n\tif (len >= size) {\n\t\tlen = size - 1;\n\t}\n\tmemcpy(buffer, message, len);\n\tbuffer[len] = '\\0';\n\treturn buffer;\n}\n\nvoid\nmaterial_stream_error(struct soluna_material_stream_context ctx_, const char *error) {\n\tstruct material_stream_context_impl *ctx = stream_context(ctx_);\n\tif (ctx != NULL && ctx->error == NULL) {\n\t\tctx->error = copy_error(ctx->error_buffer, sizeof(ctx->error_buffer), error);\n\t}\n}\n\nint\nmaterial_stream_failed(struct soluna_material_stream_context ctx_) {\n\tstruct material_stream_context_impl *ctx = stream_context(ctx_);\n\treturn ctx == NULL || ctx->error != NULL;\n}\n\nstatic soluna_material_error\nsubmit_material_stride(const void *data_, int prim_n, int material_id, int batch_n, void *ud, material_submit_stride_func submit, size_t stride) {\n\tconst char *data = (const char *)data_;\n\tif (data == NULL) {\n\t\treturn \"Missing material stream\";\n\t}\n\tif (material_id <= 0) {\n\t\treturn \"Invalid material id\";\n\t}\n\tif (batch_n <= 0) {\n\t\treturn \"Invalid material submit batch\";\n\t}\n\tif (prim_n < 0) {\n\t\treturn \"Invalid material primitive count\";\n\t}\n\tif (submit == NULL) {\n\t\treturn \"Missing material submit function\";\n\t}\n\tif (stride == 0) {\n\t\treturn \"Invalid material submit stride\";\n\t}\n\tint i = 0;\n\tfor (;;) {\n\t\tint n = prim_n - i;\n\t\tstruct material_stream_context_impl impl = {\n\t\t\t.data = data,\n\t\t\t.n = n > batch_n ? batch_n : n,\n\t\t\t.material_id = material_id,\n\t\t\t.error = NULL,\n\t\t\t.error_buffer = { 0 },\n\t\t};\n\t\tstruct soluna_material_stream_context ctx = {\n\t\t\t.ctx = &impl,\n\t\t};\n\t\tif (n > batch_n) {\n\t\t\tsubmit(ud, ctx, batch_n);\n\t\t\tif (impl.error != NULL) {\n\t\t\t\treturn copy_error(submit_error_buffer, sizeof(submit_error_buffer), impl.error);\n\t\t\t}\n\t\t\ti += batch_n;\n\t\t\tdata += stride * batch_n;\n\t\t} else {\n\t\t\tsubmit(ud, ctx, n);\n\t\t\tif (impl.error != NULL) {\n\t\t\t\treturn copy_error(submit_error_buffer, sizeof(submit_error_buffer), impl.error);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn NULL;\n}\n\nsoluna_material_error\nmaterial_submit(const void *stream, int prim_n, int material_id, int batch_n, void *ud, soluna_material_submit_func submit) {\n\treturn submit_material_stride(stream, prim_n, material_id, batch_n, ud, submit, sizeof(struct draw_primitive) * 2);\n}\n\nint\nmaterial_sprite_rect(struct soluna_sprite_bank bank, int sprite, struct soluna_sprite_rect *out) {\n\tstruct sprite_bank *b = (struct sprite_bank *)bank.ctx;\n\tif (b == NULL || out == NULL || sprite < 0 || sprite >= b->n) {\n\t\treturn 0;\n\t}\n\tstruct sprite_rect *r = &b->rect[sprite];\n\tout->texture = r->texid;\n\tout->u = (float)(r->u >> 16);\n\tout->v = (float)(r->v >> 16);\n\tout->w = (float)(r->u & 0xffffu);\n\tout->h = (float)(r->v & 0xffffu);\n\tout->ox = (float)((r->off >> 16) & 0xffffu) - 0x8000;\n\tout->oy = (float)(r->off & 0xffffu) - 0x8000;\n\treturn 1;\n}\n\nsg_bindings\nmaterial_bindings(struct soluna_render_bindings bindings) {\n\tstruct render_bindings *b = (struct render_bindings *)bindings.ctx;\n\tassert(b != NULL);\n\treturn b->bindings;\n}\n\nstatic size_t\nstream_payload_max(void) {\n\treturn sizeof(struct draw_primitive) - sizeof(struct draw_primitive_external);\n}\n\nvoid\nmaterial_stream_free(void *ptr) {\n\tfree(ptr);\n}\n\nsoluna_material_error\nmaterial_push_stream(int material_id, int count, size_t payload_size, soluna_material_stream_write_func write, void *ud, struct soluna_material_stream *out) {\n\tsize_t payload_max = stream_payload_max();\n\tif (out == NULL) {\n\t\treturn \"Missing material stream output\";\n\t}\n\tout->data = NULL;\n\tout->size = 0;\n\tif (material_id <= 0) {\n\t\treturn \"Invalid material id\";\n\t}\n\tif (count < 0) {\n\t\treturn \"Invalid material stream count\";\n\t}\n\tif (payload_size > payload_max) {\n\t\treturn \"Invalid material payload size\";\n\t}\n\tif (write == NULL) {\n\t\treturn \"Missing material stream writer\";\n\t}\n\tsize_t item_size = sizeof(struct draw_primitive) * 2;\n\tif ((size_t)count > (~(size_t)0 - 1) / item_size) {\n\t\treturn \"Material stream is too large\";\n\t}\n\tsize_t stream_size = item_size * (size_t)count;\n\tchar *buffer = (char *)malloc(stream_size + 1);\n\tif (buffer == NULL) {\n\t\treturn \"No memory for material stream\";\n\t}\n\tstruct draw_primitive *stream = (struct draw_primitive *)buffer;\n\tint i;\n\tfor (i=0; i<count; i++) {\n\t\tstruct draw_primitive *pos = &stream[i * 2];\n\t\tstruct draw_primitive *ext_prim = pos + 1;\n\t\tstruct draw_primitive_external *ext = (struct draw_primitive_external *)ext_prim;\n\t\tstruct soluna_material_stream_item item = {\n\t\t\t.x = 0.0f,\n\t\t\t.y = 0.0f,\n\t\t\t.sprite = -1,\n\t\t\t.payload = NULL,\n\t\t};\n\t\tmemset(pos, 0, sizeof(*pos));\n\t\tmemset(ext_prim, 0, sizeof(*ext_prim));\n\t\twrite(ud, i, &item);\n\t\tif (payload_size > 0) {\n\t\t\tif (item.payload == NULL) {\n\t\t\t\tmaterial_stream_free(buffer);\n\t\t\t\treturn \"Missing material stream payload\";\n\t\t\t}\n\t\t\tmemcpy((char *)ext_prim + sizeof(*ext), item.payload, payload_size);\n\t\t}\n\t\tpos->x = (int32_t)(item.x * STREAM_FIX_SCALE);\n\t\tpos->y = (int32_t)(item.y * STREAM_FIX_SCALE);\n\t\tpos->sprite = -material_id;\n\t\text->sprite = item.sprite;\n\t}\n\tbuffer[stream_size] = '\\0';\n\tout->data = buffer;\n\tout->size = stream_size;\n\treturn NULL;\n}\n\nstatic void\nclear_stream_read(size_t payload_size, void *payload, struct soluna_material_stream_data *out) {\n\tif (out != NULL) {\n\t\tmemset(out, 0, sizeof(*out));\n\t}\n\tif (payload != NULL && payload_size > 0 && payload_size <= stream_payload_max()) {\n\t\tmemset(payload, 0, payload_size);\n\t}\n}\n\nstatic int\nfail_stream_read(struct soluna_material_stream_context ctx, const char *error, size_t payload_size, void *payload, struct soluna_material_stream_data *out) {\n\tmaterial_stream_error(ctx, error);\n\tclear_stream_read(payload_size, payload, out);\n\treturn 0;\n}\n\nint\nmaterial_stream_read(struct soluna_material_stream_context ctx_, int index, size_t payload_size, void *payload, struct soluna_material_stream_data *out) {\n\tstruct material_stream_context_impl *ctx = stream_context(ctx_);\n\tsize_t payload_max = stream_payload_max();\n\tif (ctx == NULL) {\n\t\tclear_stream_read(payload_size, payload, out);\n\t\treturn 0;\n\t}\n\tif (ctx->error != NULL) {\n\t\tclear_stream_read(payload_size, payload, out);\n\t\treturn 0;\n\t}\n\tif (payload_size > payload_max) {\n\t\treturn fail_stream_read(ctx_, \"Invalid material payload size\", payload_size, payload, out);\n\t}\n\tif (ctx->data == NULL) {\n\t\treturn fail_stream_read(ctx_, \"Missing material stream\", payload_size, payload, out);\n\t}\n\tif (index < 0) {\n\t\treturn fail_stream_read(ctx_, \"Invalid material stream index\", payload_size, payload, out);\n\t}\n\tif (index >= ctx->n) {\n\t\treturn fail_stream_read(ctx_, \"Invalid material stream index\", payload_size, payload, out);\n\t}\n\tif (ctx->material_id <= 0) {\n\t\treturn fail_stream_read(ctx_, \"Invalid material id\", payload_size, payload, out);\n\t}\n\tif (out == NULL) {\n\t\treturn fail_stream_read(ctx_, \"Missing material stream output\", payload_size, payload, out);\n\t}\n\tif (payload_size > 0 && payload == NULL) {\n\t\treturn fail_stream_read(ctx_, \"Missing material stream payload output\", payload_size, payload, out);\n\t}\n\tconst struct draw_primitive *prim = (const struct draw_primitive *)ctx->data;\n\tconst struct draw_primitive *pos = &prim[index * 2];\n\tconst struct draw_primitive *ext_prim = pos + 1;\n\tconst struct draw_primitive_external *ext = (const struct draw_primitive_external *)ext_prim;\n\tif (pos->sprite != -ctx->material_id) {\n\t\treturn fail_stream_read(ctx_, \"Invalid material marker\", payload_size, payload, out);\n\t}\n\tout->x = (float)pos->x * STREAM_FIX_INV_SCALE;\n\tout->y = (float)pos->y * STREAM_FIX_INV_SCALE;\n\tout->sprite = ext->sprite;\n\tif (payload_size > 0) {\n\t\tmemcpy(payload, (const char *)ext_prim + sizeof(*ext), payload_size);\n\t}\n\treturn 1;\n}\n"
  },
  {
    "path": "src/extapi_types.h",
    "content": "// AUTO GENERATED by ../src/extapi_types.temp.h, DONT EDIT\n\n#ifndef SOLUNA_EXTAPI_TYPES_H\n#define SOLUNA_EXTAPI_TYPES_H\n\n#include <stddef.h>\n\n#include \"sokol/sokol_gfx.h\"\n\n#define SOLUNA_EXT_API_VERSION 1\n\nstruct soluna_sprite_rect {\n\tint texture;\n\tfloat u;\n\tfloat v;\n\tfloat w;\n\tfloat h;\n\tfloat ox;\n\tfloat oy;\n};\n\nstruct soluna_material_stream_item {\n\tfloat x;\n\tfloat y;\n\tint sprite;\n\tconst void *payload;\n};\n\nstruct soluna_material_stream_data {\n\tfloat x;\n\tfloat y;\n\tint sprite;\n};\n\nstruct soluna_material_stream {\n\tchar *data;\n\tsize_t size;\n};\n\ntypedef const char *soluna_material_error;\n\nstruct soluna_material_stream_context {\n\tvoid *ctx;\n};\n\nstruct soluna_render_bindings {\n\tvoid *ctx;\n};\n\nstruct soluna_sprite_bank {\n\tvoid *ctx;\n};\n\ntypedef void (*soluna_material_submit_func)(void *ud, struct soluna_material_stream_context ctx, int n);\ntypedef void (*soluna_material_stream_write_func)(void *ud, int index, struct soluna_material_stream_item *item);\n\n\n#endif\n"
  },
  {
    "path": "src/extapi_types.temp.h",
    "content": "#ifndef SOLUNA_EXTAPI_TYPES_H\n#define SOLUNA_EXTAPI_TYPES_H\n\n$HOST_TYPE_DECL$\n\n#endif\n"
  },
  {
    "path": "src/external.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n#include <stdlib.h>\n#include <string.h>\n\nstruct lua_api;\nstruct sokol_api;\nstruct soluna_api;\nextern struct lua_api * extlua_api();\nextern struct sokol_api * extlua_sokol_api();\nextern struct soluna_api * extlua_soluna_api();\n\nstruct extlua_apis {\n\tstruct lua_api * lua;\n\tstruct sokol_api * sokol;\n\tstruct soluna_api * soluna;\n};\n\nstatic struct extlua_apis *\nhost_apis() {\n\tstatic struct extlua_apis apis;\n\tapis.lua = extlua_api();\n\tapis.sokol = extlua_sokol_api();\n\tapis.soluna = extlua_soluna_api();\n\treturn &apis;\n}\n\nstatic void\ninit_extraspace(lua_State *L) {\n\tstruct extlua_apis **ex = (struct extlua_apis **)lua_getextraspace(L);\n\t*ex = host_apis();\n}\n\nstatic int\nget_reg(lua_State *L) {\n\tluaL_Reg *l = (luaL_Reg *)lua_touserdata(L, 1);\n\tlua_pushnil(L);\n\tint i = 0;\n\twhile (lua_next(L, 2) != 0) {\n\t\tl[i].name = lua_tostring(L, -2);\n\t\tl[i].func = lua_tocfunction(L, -1);\n\t\tif (l[i].name == NULL || l[i].func == NULL) {\n\t\t\treturn luaL_error(L, \"Invalid reg table\");\n\t\t}\n\t\tlua_pop(L, 1);\n\t\t++i;\n\t}\n\treturn 0;\n}\n\nstatic int\ncount_table(lua_State *L, int idx) {\n\tidx = lua_absindex(L, idx);\n\tlua_pushnil(L);\n\tint n = 0;\n\twhile (lua_next(L, idx) != 0) {\n\t\t++n;\n\t\tlua_pop(L, 1);\n\t}\n\treturn n;\n}\n\nstatic void\nregister_libs(lua_State *L, lua_State *dL, int loadonly) {\n\tif (lua_gettop(dL) == 0 || lua_type(dL, 1) != LUA_TTABLE) {\n\t\tluaL_error(L, \"Invalid external libs, maybe lua version mismatch\");\n\t}\n\tint n = count_table(dL, 1);\n\tint tbl_index = lua_gettop(L);\n\tluaL_Reg *l = lua_newuserdatauv(L, sizeof(luaL_Reg) * n, 0);\n\tlua_pushcfunction(dL, get_reg);\n\tlua_pushlightuserdata(dL, (void *)l);\n\tlua_pushvalue(dL, 1);\n\tif (lua_pcall(dL, 2, 0, 0) != LUA_OK) {\n\t\tlua_pushstring(L, lua_tostring(dL, -1));\n\t\tlua_error(L);\n\t}\n\tint i;\n\tif (loadonly) {\n\t\tfor (i=0;i<n;i++) {\n\t\t\tlua_pushcfunction(L, l[i].func);\n\t\t\tlua_setfield(L, tbl_index, l[i].name);\n\t\t}\n\t} else {\n\t\tfor (i=0;i<n;i++) {\n\t\t\tluaL_requiref (L, l[i].name, l[i].func, 0);\n\t\t\tlua_setfield(L, tbl_index, l[i].name);\n\t\t}\n\t}\n\tlua_settop(L, tbl_index);\n}\n\nstatic int\nregister_libs_(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TTABLE);\n\tlua_State *dL = (lua_State *)lua_touserdata(L, 2);\n\tint loadonly = lua_toboolean(L, 3);\n\tlua_settop(L, 1);\n\tregister_libs(L, dL, loadonly);\n\treturn 1;\n}\n\nstatic int\nload_libs(lua_State *L) {\n\tlua_State *dL = luaL_newstate();\n\tinit_extraspace(dL);\n\tlua_CFunction init = lua_tocfunction(L, 1);\n\tif (init == NULL)\n\t\treturn luaL_error(L, \"Need C function\");\n\tinit(dL);\n\tlua_pushcfunction(L, register_libs_);\n\tlua_newtable(L);\n\tlua_pushlightuserdata(L, dL);\n\tint ok = lua_pcall(L, 2, 1, 0);\n\tlua_close(dL);\n\tif (ok != LUA_OK) {\n\t\tlua_error(L);\n\t}\n\treturn 1;\n}\n\nstruct preload_extlib {\n\tint n;\n\tluaL_Reg *l;\n};\n\nstatic struct preload_extlib PRELOAD;\n\nstatic void\npreload_lib(lua_State *L, lua_CFunction init, int result_index) {\n\tlua_State *dL = luaL_newstate();\n\tinit_extraspace(dL);\n\tinit(dL);\n\tlua_pushcfunction(L, register_libs_);\n\tlua_pushvalue(L, result_index);\n\tlua_pushlightuserdata(L, dL);\n\tlua_pushboolean(L, 1);\t// loadonly\n\tint ok = lua_pcall(L, 3, 1, 0);\n\tlua_close(dL);\n\tif (ok != LUA_OK) {\n\t\tlua_error(L);\n\t}\n}\n\nstatic int\ncmpreg(const void *a, const void *b) {\n\tconst luaL_Reg *ra = (const luaL_Reg *)a;\n\tconst luaL_Reg *rb = (const luaL_Reg *)b;\n\treturn strcmp(ra->name, rb->name);\n}\n\nstatic int\npreload_libs(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TTABLE);\n\tif (PRELOAD.l != NULL) {\n\t\treturn luaL_error(L, \"Already preload\");\n\t}\n\tlua_newtable(L);\n\tint result_index = lua_gettop(L);\n\tint i=0;\n\twhile (lua_geti(L, 1, ++i) != LUA_TNIL) {\n\t\tlua_CFunction init = lua_tocfunction(L, -1);\n\t\tif (init == NULL)\n\t\t\treturn luaL_error(L, \"Invalid init function at %d\", i);\n\t\tlua_pop(L, 1);\n\t\tpreload_lib(L, init, result_index);\n\t}\n\tlua_pop(L, 1);\n\tint n = count_table(L, result_index);\n\tstruct luaL_Reg *l = lua_newuserdatauv(L, sizeof(luaL_Reg) * n, 1);\n\t\n\t// ref name strings\n\tlua_pushvalue(L, result_index);\n\tlua_setiuservalue(L, -2, 1);\n\t\n\tlua_pushcfunction(L, get_reg);\n\tlua_pushvalue(L, -2);\t// l\n\tlua_pushvalue(L, result_index);\n\tlua_call(L, 2, 0);\n\n\tqsort(l, n, sizeof(luaL_Reg), cmpreg);\n\n\tPRELOAD.l = l;\n\tPRELOAD.n = n;\n\t\n\tlua_setfield(L, LUA_REGISTRYINDEX, \"EXTLIBS\");\n\n\treturn 1;\n}\n\nstatic lua_CFunction\nfind_func(luaL_Reg *l, int n, const char *name) {\n\tint begin = 0;\n\tint end = n;\n\twhile (begin < end) {\n\t\tint mid = (begin + end) / 2;\n\t\tint c = strcmp(name, l[mid].name);\n\t\tif (c == 0)\n\t\t\treturn l[mid].func;\n\t\telse if (c < 0) {\n\t\t\tend = mid;\n\t\t} else {\n\t\t\tbegin = mid + 1;\n\t\t}\n\t}\n\treturn NULL;\n}\n\nstatic int\nsearcher(lua_State *L) {\n\tif (PRELOAD.l == NULL || PRELOAD.n == 0)\n\t\treturn 0;\n\tif (lua_gettop(L) == 0) {\n\t\t// test preload table\n\t\tlua_pushboolean(L, 1);\n\t\treturn 1;\n\t}\n\tconst char * name = lua_tostring(L, 1);\n\tlua_CFunction func = find_func(PRELOAD.l, PRELOAD.n, name);\n\tif (func == NULL) {\n\t\tlua_pushfstring(L, \"No preload extlua '%s'\", name);\n\t\treturn 1;\n\t}\n\tlua_pushcfunction(L, func);\n\treturn 1;\n}\n\nint\nluaopen_extlua(lua_State *L) {\n\tluaL_Reg l[] = {\n\t\t{ \"load\", load_libs },\n\t\t{ \"preload\", preload_libs },\n\t\t{ \"searcher\", searcher },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\treturn 1;\n}\n"
  },
  {
    "path": "src/file.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n#include <stdlib.h>\n#include <string.h>\n\nFILE * fopen_utf8(const char *filename, const char *mode);\n\nstatic int\nlfile_exist(lua_State *L) {\n\tconst char *filename = luaL_checkstring(L, 1);\n\tconst char *mode = \"rb\";\n\tFILE *f = fopen_utf8(filename, mode);\n\tif (f == NULL)\n\t\treturn 0;\n\tfclose(f);\n\tlua_pushboolean(L, 1);\n\treturn 1;\n}\n\nstatic void *\nexternal_free(void *ud, void *ptr, size_t osize, size_t nsize) {\n\tfree(ptr);\n\treturn NULL;\n}\n\nstatic int\nlfile_load(lua_State *L) {\n\tconst char *filename = luaL_checkstring(L, 1);\n\tconst char *mode = luaL_optstring(L, 2, \"rb\");\n\tFILE *f = fopen_utf8(filename, mode);\n\tif (f == NULL)\n\t\treturn 0;\n\tfseek(f, 0, SEEK_END);\n\tsize_t sz = ftell(f);\n\tfseek(f, 0, SEEK_SET);\n\tchar * buffer = (char *)malloc(sz+1);\n\tif (buffer == NULL) {\n\t\tfclose(f);\n\t\treturn luaL_error(L, \"lfile_load_string : Out of memory\");\n\t}\n\tbuffer[sz] = 0;\n\tsize_t rd = fread(buffer, 1, sz, f);\n\tfclose(f);\n\t\n\tif (rd != sz) {\n\t\tfree(buffer);\n\t\treturn luaL_error(L, \"Read %s fail\", filename);\n\t}\n\tlua_pushexternalstring(L, buffer, sz, external_free, NULL);\n\treturn 1;\n}\n\nint\nluaopen_soluna_file(lua_State *L) {\n\tluaL_checkversion(L);\n\tluaL_Reg l[] = {\n\t\t{ \"exist\", lfile_exist },\n\t\t{ \"load\", lfile_load },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\treturn 1;\n}\n"
  },
  {
    "path": "src/font.c",
    "content": "#include \"font_manager.h\"\n#include \"truetype.h\"\n\n#include <lua.h>\n#include <lauxlib.h>\n\n#include <string.h>\n#include <stdint.h>\n#include <stdlib.h>\n#include <assert.h>\n#include <ctype.h>\n\n#include \"luabuffer.h\"\n#include \"stb/stb_image_write.h\"\n\nstatic struct {\n\tstruct font_manager *mgr;\n} G;\n\nstatic struct font_manager* \ngetF(lua_State *L) {\n\tif (G.mgr == NULL)\n\t\tluaL_error(L, \"Init font manager first\");\n\treturn G.mgr;\n}\n\nstatic int\nlsubmit(lua_State *L){\n\tstruct font_manager *F = getF(L);\n\tint dirty = font_manager_flush(F);\n\tlua_pushboolean(L, dirty);\n\treturn 1;\n}\n\nstatic int\nlimport(lua_State *L) {\n\tstruct font_manager *F = getF(L);\n\tsize_t sz;\n\tvoid* fontdata = (void *)luaL_getbuffer(L, &sz);\n\tfont_manager_import(F, fontdata, sz);\n\treturn 0;\n}\n\nstatic int\nlname(lua_State *L) {\n\tstruct font_manager *F = getF(L);\n\tconst char* family = luaL_checkstring(L, 1);\n\tconst int fontid = font_manager_addfont_with_family(F, family);\n\tif (fontid > 0){\n\t\tlua_pushinteger(L, fontid);\n\t\treturn 1;\n\t}\n\treturn 0;\n}\n\nstatic int\nltexture(lua_State *L) {\n\tstruct font_manager *F = getF(L);\n\tint size = 0;\n\tconst void * ptr = font_manager_texture(F, &size);\n\tlua_pushlightuserdata(L, (void *)ptr);\n\tlua_pushinteger(L, size * size);\n\treturn 2;\n}\n\nstatic int\nltexture_write(lua_State *L) {\n\tstruct font_manager *F = getF(L);\n\tint size = 0;\n\tconst void * ptr = font_manager_texture(F, &size);\n\tconst char * filename = luaL_checkstring(L, 1);\n\tif (!stbi_write_png(filename, size, size, 1, ptr, size)) {\n\t\treturn luaL_error(L, \"Write %s failed\", filename);\n\t}\n\treturn 0;\n}\n\nstatic int\nltouch(lua_State *L) {\n\tstruct font_manager *F = getF(L);\n\tint fontid = luaL_checkinteger(L, 1);\n\tint codepoint = luaL_checkinteger(L, 2);\n\tstruct font_glyph tmp1, tmp2;\n\tfont_manager_glyph(F, fontid, codepoint, 16, &tmp1, &tmp2);\n\treturn 0;\n}\n\nstatic int\nlcobj(lua_State *L) {\n\tstruct font_manager *F = getF(L);\n\tlua_pushlightuserdata(L, (void *)F);\n\treturn 1;\n}\n\nstatic int\nlimport_icon(lua_State *L) {\n\tstruct font_manager *F = getF(L);\n\tluaL_checktype(L, 1, LUA_TUSERDATA);\n\tvoid *data = lua_touserdata(L, 1);\n\tsize_t sz = lua_rawlen(L, 1);\n\tint n = sz / FONT_MANAGER_GLYPHSIZE / FONT_MANAGER_GLYPHSIZE;\n\tif (n * FONT_MANAGER_GLYPHSIZE * FONT_MANAGER_GLYPHSIZE != sz)\n\t\treturn luaL_error(L, \"Invalid icon bundle size\");\n\tfont_manager_icon_init(F, n, data);\n\treturn 0;\n}\n\nstatic int\nlsize(lua_State *L) {\n\tstruct font_manager *F = getF(L);\n\tint font_id = luaL_checkinteger(L, 1);\n\tif (font_id <= 0) {\n\t\treturn luaL_error(L, \"Invalid font_id %d\", font_id);\n\t}\n\tint size = luaL_checkinteger(L, 2);\n\tint ascent, descent, lineGap;\n\tfont_manager_fontheight(F, font_id, size, &ascent, &descent, &lineGap);\n\tif (!lua_istable(L, 3)) {\n\t\tlua_settop(L, 2);\n\t\tlua_createtable(L, 0, 3);\n\t} else {\n\t\tlua_settop(L, 3);\n\t}\n\tlua_pushinteger(L, ascent);\n\tlua_setfield(L, 3, \"ascent\");\n\tlua_pushinteger(L, descent);\n\tlua_setfield(L, 3, \"descent\");\n\tlua_pushinteger(L, lineGap);\n\tlua_setfield(L, 3, \"lineGap\");\n\treturn 1;\n}\n\n#define MAX_FONTNAME 256\n\nstatic int\nllist(lua_State *L) {\n\tstruct font_manager *F = getF(L);\n\tint i = 1;\n\tlua_newtable(L);\n\tchar buf[MAX_FONTNAME];\n\tfor (;;) {\n\t\tint r = font_manager_enum_fontname(F, i, buf, MAX_FONTNAME);\n\t\tif (r <= 0)\n\t\t\tbreak;\n\t\tif (r >= MAX_FONTNAME) {\n\t\t\tchar *tmp = (char *)malloc(r + 1);\n\t\t\tfont_manager_enum_fontname(F, i, tmp, r+1);\n\t\t\tlua_pushlstring(L, tmp, r);\n\t\t\tfree(tmp);\n\t\t} else {\n\t\t\tlua_pushlstring(L, buf, r);\n\t\t}\n\t\tlua_seti(L, -2, i);\n\t\t++i;\n\t}\n\treturn 1;\n}\n\nint\nluaopen_font(lua_State *L) {\n\tluaL_checkversion(L);\n\t\n\tluaL_Reg l[] = {\n\t\t{ \"texture\",\t\t\tltexture },\n\t\t{ \"texture_write\",\t\tltexture_write },\n\t\t{ \"touch\",\t\t\t\tltouch },\t// for debug\n\t\t{ \"import\",\t\t\t\tlimport },\n\t\t{ \"name\",\t\t\t\tlname },\n\t\t{ \"size\",\t\t\t\tlsize },\n\t\t{ \"submit\",\t\t\t\tlsubmit },\n\t\t{ \"cobj\",\t\t\t\tlcobj },\n\t\t{ \"texture_size\",\t\tNULL },\n\t\t{ \"import_icon\",\t\tlimport_icon },\n\t\t{ \"list\",\t\t\t\tllist },\n\t\t{ NULL, \t\t\t\tNULL },\n\t};\n\t\n\tluaL_newlib(L, l);\n\n\tlua_pushinteger(L, FONT_MANAGER_TEXSIZE);\n\tlua_setfield(L, -2, \"texture_size\");\n\n\treturn 1;\n}\n\nvoid soluna_openlibs(lua_State *L);\n\nstatic int\nluavm_init(lua_State *L) {\n\tsoluna_openlibs(L);\n\tconst char* data = (const char*)lua_touserdata(L, 1);\n\tsize_t size = (size_t)lua_tointeger(L, 2);\n\tconst char* chunkname = (const char*)lua_touserdata(L, 3);\n\tif (luaL_loadbuffer(L, data, size, chunkname) != LUA_OK) {\n\t\treturn lua_error(L);\n\t}\n\tlua_call(L, 0, 0);\n\treturn 0;\n}\n\nstatic int\nfontm_init(lua_State *L) {\n\tif (G.mgr != NULL) {\n\t\treturn luaL_error(L, \"Do not init font manager twice\");\n\t}\n\tstruct font_manager* F = (struct font_manager *)malloc(font_manager_sizeof());\n\tif (F == NULL) {\n\t\treturn luaL_error(L, \"not enough memory\");\n\t}\n\tsize_t sz;\n\tconst char * src = luaL_checklstring(L, 1, &sz);\n\n\tlua_State* managerL = luaL_newstate();\n\tif (!managerL) {\n\t\tfree(F);\n\t\treturn luaL_error(L, \"not enough memory\");\n\t}\n\tlua_pushcfunction(managerL, luavm_init);\n\tlua_pushlightuserdata(managerL, (void *)src);\n\tlua_pushinteger(managerL, sz);\n\tlua_pushlightuserdata(managerL, (void*)luaL_checkstring(L, 2));\n\tif (lua_pcall(managerL, 3, 0, 0) != LUA_OK) {\n\t\tlua_pushstring(L, lua_tostring(managerL, -1));\n\t\tlua_close(managerL);\n\t\tfree(F);\n\t\treturn lua_error(L);\n\t}\n\tfont_manager_init(F, managerL);\n\t\n\tG.mgr = F;\t\n\treturn 0;\n}\n\nstatic int\nfontm_shutdown(lua_State *L) {\n\tstruct font_manager* F = G.mgr;\n\tif (F == NULL)\n\t\treturn 0;\n\tG.mgr = NULL;\n\tvoid* managerL = font_manager_shutdown(F);\n\tif (managerL) {\n\t\tlua_close((lua_State*)managerL);\n\t}\n\tfree(F);\n\treturn 0;\n}\n\nint\nluaopen_font_manager(lua_State *L) {\n\tluaL_checkversion(L);\n\tluaL_Reg l[] = {\n\t\t{ \"init\", fontm_init },\n\t\t{ \"shutdown\", fontm_shutdown },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\n\treturn 1;\n}\n"
  },
  {
    "path": "src/font_define.h",
    "content": "#ifndef soluna_font_define_h\n#define soluna_font_define_h\n\n#define FONT_MANAGER_TEXSIZE 2048\n#define FONT_MANAGER_GLYPHSIZE 64\n#define FONT_POSTION_FIX_POINT  8\n\n#define MAX_FONT_NUM 64\n\n#include <stdint.h>\nstruct font_glyph {\n\tint16_t offset_x;\n\tint16_t offset_y;\n\tint16_t advance_x;\n\tint16_t advance_y;\n\tuint16_t w;\n\tuint16_t h;\n\tuint16_t u;\n\tuint16_t v;\n};\n\n#define IMAGE_FONT_MASK 0x40    //7 bit\n#define FONT_ID_MASK    0x3F    //low 6 bits\n#define FONT_ICON       255\n\nstatic inline uint32_t\ncodepoint_key(int font, int codepoint) {\n\treturn (uint32_t)((font << 24) | codepoint);\n}\n\nstatic inline int\nfont_index(int fontid){\n    return FONT_ID_MASK&((uint8_t)fontid);\n}\n\n#endif"
  },
  {
    "path": "src/font_manager.c",
    "content": "#include \"font_manager.h\"\n#include \"mutex.h\"\n#include \"truetype.h\"\n\n#include <string.h>\n#include <stdio.h>\n#include <assert.h>\n#include <stdlib.h>\n\n#define STB_TRUETYPE_IMPLEMENTATION\n#include <stb/stb_truetype.h>\n\n#define FONT_MANAGER_SLOTLINE (FONT_MANAGER_TEXSIZE/FONT_MANAGER_GLYPHSIZE)\n#define FONT_MANAGER_SLOTS (FONT_MANAGER_SLOTLINE*FONT_MANAGER_SLOTLINE)\n#define FONT_MANAGER_HASHSLOTS (FONT_MANAGER_SLOTS * 2)\n\n\n// --------------\n//\n//                       xmin                     xmax\n//                        |                         |\n//                        |<-------- width -------->|\n//                        |                         |\n//              |         +-------------------------+----------------- ymax\n//              |         |    ggggggggg   ggggg    |     ^        ^\n//              |         |   g:::::::::ggg::::g    |     |        |\n//              |         |  g:::::::::::::::::g    |     |        |\n//              |         | g::::::ggggg::::::gg    |     |        |\n//              |         | g:::::g     g:::::g     |     |        |\n//    offset_x -|-------->| g:::::g     g:::::g     |  offset_y    |\n//              |         | g:::::g     g:::::g     |     |        |\n//              |         | g::::::g    g:::::g     |     |        |\n//              |         | g:::::::ggggg:::::g     |     |        |\n//              |         |  g::::::::::::::::g     |     |      height\n//              |         |   gg::::::::::::::g     |     |        |\n//  baseline ---*---------|---- gggggggg::::::g-----*--------      |\n//            / |         |             g:::::g     |              |\n//     origin   |         | g:::::gg   gg:::::g     |              |\n//              |         | g:::::gg   gg:::::g     |              |\n//              |         |  g::::::ggg:::::::g     |              |\n//              |         |   gg:::::::::::::g      |              |\n//              |         |     ggg::::::ggg        |              |\n//              |         |         gggggg          |              v\n//              |         +-------------------------+----------------- ymin\n//              |                                   |\n//              |------------- advance_x ---------->|\n\nstruct font_slot {\n\tuint32_t codepoint_key;\t// high 8 bits (ttf index)\n\tint16_t offset_x;\n\tint16_t offset_y;\n\tint16_t advance_x;\n\tint16_t advance_y;\n\tuint16_t w;\n\tuint16_t h;\n};\n\nstruct priority_list {\n\tint version;\n\tint16_t prev;\n\tint16_t next;\n};\n\nstruct truetype_font;\n\nstruct font_manager {\n\tint version;\n\tint count;\n\tint16_t list_head;\n\tstruct font_slot slots[FONT_MANAGER_SLOTS];\n\tstruct priority_list priority[FONT_MANAGER_SLOTS];\n\tint16_t hash[FONT_MANAGER_HASHSLOTS];\n\tstruct truetype_font* ttf;\n\tvoid *L;\n\tint dpi_perinch;\n\tint dirty;\n\tint icon_n;\n\tunsigned char *icon_data;\n\tmutex_t mutex;\n\tuint8_t texture_buffer[FONT_MANAGER_TEXSIZE*FONT_MANAGER_TEXSIZE];\n};\n\nconst void *\nfont_manager_texture(struct font_manager *F, int *sz) {\n\t*sz = FONT_MANAGER_TEXSIZE;\n\treturn F->texture_buffer;\n}\n\n/*\n\tF->priority is a circular linked list for the LRU cache.\n\tF->hash is for lookup with [font, codepoint].\n*/\n\n#define COLLISION_STEP 7\n#define DISTANCE_OFFSET 8\n#define ORIGINAL_SIZE (FONT_MANAGER_GLYPHSIZE - DISTANCE_OFFSET * 2)\n#define ONEDGE_VALUE\t180\n#define PIXEL_DIST_SCALE (ONEDGE_VALUE/(float)(DISTANCE_OFFSET))\n\nstatic const int SAPCE_CODEPOINT[] = {\n    ' ', '\\t', '\\n', '\\r',\n};\n\nstatic inline int\nis_space_codepoint(int codepoint){\n    for (int ii=0; ii < sizeof(SAPCE_CODEPOINT)/sizeof(SAPCE_CODEPOINT[0]); ++ii){\n        if (codepoint == SAPCE_CODEPOINT[ii]){\n            return 1;\n        }\n    }\n    return 0;\n}\n\nstatic inline void\nlock(struct font_manager *F) {\n\tmutex_acquire(F->mutex);\n}\n\nstatic inline void\nunlock(struct font_manager *F) {\n\tmutex_release(F->mutex);\n}\n\nstatic inline const stbtt_fontinfo*\nget_ttf_unsafe(struct font_manager *F, int fontid){\n\treturn truetype_font(F->ttf, fontid, F->L);\n}\n\nstatic inline const stbtt_fontinfo *\nget_ttf(struct font_manager *F, int fontid) {\n\tlock(F);\n\tconst stbtt_fontinfo * r = get_ttf_unsafe(F, fontid);\n\tunlock(F);\n\treturn r;\n}\n\nstatic inline int\nttf_with_family(struct font_manager *F, const char* family){\n\treturn truetype_name(F->L, family);\n}\n\nstatic inline int\nhash(int value) {\n\treturn (value * 0xdeece66d + 0xb) % FONT_MANAGER_HASHSLOTS;\n}\n\nstatic int\nhash_lookup(struct font_manager *F, int cp) {\n\tint slot;\n\tint position = hash(cp);\n\twhile ((slot = F->hash[position]) >= 0) {\n\t\tstruct font_slot * s = &F->slots[slot];\n\t\tif (s->codepoint_key == cp)\n\t\t\treturn slot;\n\t\tposition = (position + COLLISION_STEP) % FONT_MANAGER_HASHSLOTS;\n\t}\n\treturn -1;\n}\n\nstatic void rehash(struct font_manager *F);\n\nstatic void\nhash_insert(struct font_manager *F, int cp, int slotid) {\n\t++F->count;\n\tif (F->count > FONT_MANAGER_SLOTS + FONT_MANAGER_SLOTS/2) {\n\t\trehash(F);\n\t}\n\tint position = hash(cp);\n\tint slot;\n\twhile ((slot = F->hash[position]) >= 0) {\n\t\tstruct font_slot * s = &F->slots[slot];\n\t\tif (s->codepoint_key < 0)\n\t\t\tbreak;\n\t\tassert(s->codepoint_key != cp);\n\n\t\tposition = (position + COLLISION_STEP) % FONT_MANAGER_HASHSLOTS;\n\t}\n\tF->hash[position] = slotid;\n\tF->slots[slotid].codepoint_key = cp;\n}\n\nstatic void\nrehash(struct font_manager *F) {\n\tint i;\n\tfor (i=0;i<FONT_MANAGER_HASHSLOTS;i++) {\n\t\tF->hash[i] = -1;\t// reset slots\n\t}\n\tF->count = 0;\n\tint count = 0;\n\t(void)count;\n\tfor (i=0;i<FONT_MANAGER_SLOTS;i++) {\n\t\tint cp = F->slots[i].codepoint_key;\n\t\tif (cp >= 0) {\n\t\t\tassert(++count <= FONT_MANAGER_SLOTS);\n\t\t\thash_insert(F, cp, i);\n\t\t}\n\t}\n}\n\nstatic void\nremove_node(struct font_manager *F, struct priority_list *node) {\n\tstruct priority_list *prev_node = &F->priority[node->prev];\n\tstruct priority_list *next_node = &F->priority[node->next];\n\tprev_node->next = node->next;\n\tnext_node->prev = node->prev;\n}\n\nstatic void\ntouch_slot(struct font_manager *F, int slotid) {\n\tstruct priority_list *node = &F->priority[slotid];\n\tnode->version = F->version;\n\tif (slotid == F->list_head)\n\t\treturn;\n\tremove_node(F, node);\n\t// insert before head\n\tint head = F->list_head;\n\tint tail = F->priority[head].prev;\n\tnode->prev = tail;\n\tnode->next = head;\n\tstruct priority_list *head_node = &F->priority[head];\n\tstruct priority_list *tail_node = &F->priority[tail];\n\thead_node->prev = slotid;\n\ttail_node->next = slotid;\n\tF->list_head = slotid;\n}\n\nstatic int\nget_icon(struct font_manager *F, int cp, struct font_glyph *glyph) {\n\tif (cp < 0 || cp >= F->icon_n) {\n\t\tmemset(glyph, 0, sizeof(*glyph));\n\t\treturn -1;\n\t}\n\tglyph->offset_x = 0;\n\tglyph->offset_y = -DISTANCE_OFFSET/2;\n\tglyph->advance_x = FONT_MANAGER_GLYPHSIZE;\n\tglyph->advance_y = FONT_MANAGER_GLYPHSIZE;\n\tglyph->w = FONT_MANAGER_GLYPHSIZE;\n\tglyph->h = FONT_MANAGER_GLYPHSIZE;\n\tglyph->u = 0;\n\tglyph->v = 0;\n\treturn 0;\n}\n\n// 1 exist in cache. 0 not exist in cache , call font_manager_update. -1 failed.\nstatic int\nfont_manager_touch_unsafe(struct font_manager *F, int font, int codepoint, struct font_glyph *glyph) {\n\tint cp = codepoint_key(font, codepoint);\n\tint slot = hash_lookup(F, cp);\n\tif (slot >= 0) {\n\t\ttouch_slot(F, slot);\n\t\tstruct font_slot *s = &F->slots[slot];\n\t\tglyph->offset_x = s->offset_x;\n\t\tglyph->offset_y = s->offset_y;\n\t\tglyph->advance_x = s->advance_x;\n\t\tglyph->advance_y = s->advance_y;\n\t\tglyph->w = s->w;\n\t\tglyph->h = s->h;\n\t\tglyph->u = (slot % FONT_MANAGER_SLOTLINE) * FONT_MANAGER_GLYPHSIZE;\n\t\tglyph->v = (slot / FONT_MANAGER_SLOTLINE) * FONT_MANAGER_GLYPHSIZE;\n\n\t\treturn 1;\n\t}\n\tint last_slot = F->priority[F->list_head].prev;\n\tstruct priority_list *last_node = &F->priority[last_slot];\n\t\n\tif (font == FONT_ICON) {\n\t\tif (last_node->version != F->version) {\n\t\t\tF->dirty = 1;\n\t\t}\n\t\treturn get_icon(F, codepoint, glyph);\n\t}\n\t\n\tif (font_index(font) <= 0) {\n\t\t// invalid font\n\t\tmemset(glyph, 0, sizeof(*glyph));\n\t\treturn -1;\n\t}\n\n\tconst struct stbtt_fontinfo *fi = get_ttf_unsafe(F, font);\n\n\tfloat scale = stbtt_ScaleForMappingEmToPixels(fi, ORIGINAL_SIZE);\n\tint ascent, descent, lineGap;\n\tint advance, lsb;\n\tint ix0, iy0, ix1, iy1;\n\n\tif (!stbtt_GetFontVMetricsOS2(fi, &ascent, &descent, &lineGap)) {\n\t\tstbtt_GetFontVMetrics(fi, &ascent, &descent, &lineGap);\n\t}\n\tstbtt_GetCodepointHMetrics(fi, codepoint, &advance, &lsb);\n\tstbtt_GetCodepointBitmapBox(fi, codepoint, scale, scale, &ix0, &iy0, &ix1, &iy1);\n\n\tglyph->w = ix1-ix0 + DISTANCE_OFFSET * 2;\n\tglyph->h = iy1-iy0 + DISTANCE_OFFSET * 2;\n\tglyph->offset_x = (short)(lsb * scale) - DISTANCE_OFFSET;\n\tglyph->offset_y = iy0 - DISTANCE_OFFSET;\n\tglyph->advance_x = (short)(((float)advance) * scale + 0.5f);\n\tglyph->advance_y = (short)((ascent - descent) * scale + 0.5f);\n\tglyph->u = 0;\n\tglyph->v = 0;\n\n\tif (last_node->version == F->version)\t// full ?\n\t\treturn -1;\n\t\t\n\tF->dirty = 1;\n\n\treturn 0;\n}\n\nstatic int\nfont_manager_touch(struct font_manager *F, int font, int codepoint, struct font_glyph *glyph) {\n\tlock(F);\n\tint r = font_manager_touch_unsafe(F, font, codepoint, glyph);\n\tunlock(F);\n\treturn r;\n}\n\nstatic inline int\nscale_font(int v, float scale, int size) {\n\treturn ((int)(v * scale * size) + ORIGINAL_SIZE/2) / ORIGINAL_SIZE;\n}\n\nstatic inline float\nfscale_font(float v, float scale, int size){\n\treturn (v * scale * size) / (float)ORIGINAL_SIZE;\n}\n\nvoid\nfont_manager_fontheight(struct font_manager *F, int fontid, int size, int *ascent, int *descent, int *lineGap) {\n\tif (fontid <= 0) {\n\t\t*ascent = 0;\n\t\t*descent = 0;\n\t\t*lineGap = 0;\n\t}\n\n\tconst struct stbtt_fontinfo *fi = get_ttf(F, fontid);\n\tfloat scale = stbtt_ScaleForMappingEmToPixels(fi, ORIGINAL_SIZE);\n\tif (!stbtt_GetFontVMetricsOS2(fi, ascent, descent, lineGap)) {\n\t\tstbtt_GetFontVMetrics(fi, ascent, descent, lineGap);\n\t}\n\t*ascent = scale_font(*ascent, scale, size);\n\t*descent = scale_font(*descent, scale, size);\n\t*lineGap = scale_font(*lineGap, scale, size);\n}\n\nint \nfont_manager_underline(struct font_manager *F, int fontid, int size, float *position, float *thickness){\n\tconst struct stbtt_fontinfo *fi = get_ttf(F, fontid);\n\tfloat scale = stbtt_ScaleForMappingEmToPixels(fi, ORIGINAL_SIZE);\n\tstbtt_uint32 post = stbtt__find_table(fi->data, fi->fontstart, \"post\");\n\tif (!post) {\n\t\treturn -1;\n\t}\n\tint16_t underline_position = ttSHORT(fi->data + post + 8);\n\tint16_t underline_thickness = ttSHORT(fi->data + post + 10);\n\t*position = fscale_font(underline_position, scale, size);\n\t*thickness = fscale_font(underline_thickness, scale, size);\n\treturn 0;\n}\n\n// F->dpi_perinch is a constant, so do not need to lock\nint\nfont_manager_pixelsize(struct font_manager *F, int fontid, int pointsize) {\n\t//TODO: need set dpi when init font_manager\n\tconst int defaultdpi = 96;\n\tconst int dpi = F->dpi_perinch == 0 ? defaultdpi : F->dpi_perinch;\n\treturn (int)((pointsize / 72.f) * dpi + 0.5f);\n}\n\nstatic inline void\nscale(short *v, int size) {\n\t*v = (*v * size + ORIGINAL_SIZE/2) / ORIGINAL_SIZE;\n}\n\nstatic inline void\nuscale(uint16_t *v, int size) {\n\t*v = (*v * size + ORIGINAL_SIZE/2) / ORIGINAL_SIZE;\n}\n\nvoid\nfont_manager_scale(struct font_manager *F, struct font_glyph *glyph, int size) {\n\t(void)F;\n\tscale(&glyph->offset_x, size);\n\tscale(&glyph->offset_y, size);\n\tscale(&glyph->advance_x, size);\n\tscale(&glyph->advance_y, size);\n\tuscale(&glyph->w, size);\n\tuscale(&glyph->h, size);\n}\n\nstatic void\nicon_scale(struct font_glyph *glyph, int size) {\n\tglyph->offset_x = glyph->offset_x * size / FONT_MANAGER_GLYPHSIZE;\n\tglyph->offset_y = glyph->offset_y * size / FONT_MANAGER_GLYPHSIZE;\n\tglyph->advance_x = glyph->advance_x * size / FONT_MANAGER_GLYPHSIZE;\n\tglyph->advance_y = glyph->advance_y * size / FONT_MANAGER_GLYPHSIZE;\n\tglyph->w = glyph->w * size / FONT_MANAGER_GLYPHSIZE;\n\tglyph->h = glyph->h * size / FONT_MANAGER_GLYPHSIZE;\n}\n\nstatic const char *\nfont_manager_update(struct font_manager *F, int fontid, int codepoint, struct font_glyph *glyph, uint8_t *buffer, int stride) {\n\tif (fontid <= 0)\n\t\treturn \"Invalid font\";\n\n\tlock(F);\n\t\n\tint cp = codepoint_key(fontid, codepoint);\n\tint slot = hash_lookup(F, cp);\n\tif (slot < 0) {\n\t\t// move last node to head\n\t\tslot = F->priority[F->list_head].prev;\n\t\tstruct priority_list *last_node = &F->priority[slot];\n\t\tif (last_node->version == F->version) {\t// full ?\n\t\t\tunlock(F);\n\t\t\treturn \"Too many glyph\";\n\t\t}\n\t\tlast_node->version = F->version;\n\t\tF->list_head = slot;\n\t\tF->slots[slot].codepoint_key = -1;\n\t\thash_insert(F, cp, slot);\n\t}\n\n\tglyph->u = (slot % FONT_MANAGER_SLOTLINE) * FONT_MANAGER_GLYPHSIZE;\n\tglyph->v = (slot / FONT_MANAGER_SLOTLINE) * FONT_MANAGER_GLYPHSIZE;\n\n\tstruct font_slot *s = &F->slots[slot];\n\ts->codepoint_key = cp;\n\ts->offset_x = glyph->offset_x;\n\ts->offset_y = glyph->offset_y;\n\ts->advance_x = glyph->advance_x;\n\ts->advance_y = glyph->advance_y;\n\ts->w = glyph->w;\n\ts->h = glyph->h;\n\t\n\tif (fontid == FONT_ICON) {\n\t\tif (codepoint < 0 || codepoint >= F->icon_n) {\n\t\t\tunlock(F);\n\t\t\treturn \"Invalid icon\";\n\t\t}\n\t\tunsigned char * icon_data = F->icon_data;\n\t\tunlock(F);\n\t\t\n\t\ticon_data += codepoint * FONT_MANAGER_GLYPHSIZE * FONT_MANAGER_GLYPHSIZE;\n\t\tbuffer += stride * glyph->v + glyph->u;\n\t\t\n\t\tint i;\n\t\tfor (i=0;i<FONT_MANAGER_GLYPHSIZE;i++) {\n\t\t\tmemcpy(buffer, icon_data, FONT_MANAGER_GLYPHSIZE);\n\t\t\tbuffer += stride;\n\t\t\ticon_data += FONT_MANAGER_GLYPHSIZE;\n\t\t}\n\t\t\n\t\treturn NULL;\n\t}\n\n\tconst struct stbtt_fontinfo *fi = get_ttf_unsafe(F, fontid);\n\tfloat scale = stbtt_ScaleForMappingEmToPixels(fi, ORIGINAL_SIZE);\n\n\tunlock(F);\n\t\n\tint width, height, xoff, yoff;\n\n\tunsigned char *tmp = stbtt_GetCodepointSDF(fi, scale, codepoint, DISTANCE_OFFSET, ONEDGE_VALUE, PIXEL_DIST_SCALE, &width, &height, &xoff, &yoff);\n\tif (tmp == NULL) {\n\t\treturn NULL;\n\t}\n\t\n\tconst uint8_t *src = (const uint8_t *)tmp;\n\tbuffer += stride * glyph->v + glyph->u;\n\n\tint src_stride = width;\n\tif (width > FONT_MANAGER_GLYPHSIZE)\n\t\twidth = FONT_MANAGER_GLYPHSIZE;\n\tif (height > FONT_MANAGER_GLYPHSIZE)\n\t\theight = FONT_MANAGER_GLYPHSIZE;\n\tif (width > glyph->w)\n\t\twidth = glyph->w;\n\tif (height > glyph->h)\n\t\theight = glyph->h;\n\n\tint i;\n\tfor (i=0;i<height;i++) {\n\t\tmemcpy(buffer, src, width);\n\t\tmemset(buffer + width, 0, FONT_MANAGER_GLYPHSIZE - width);\n\t\tsrc += src_stride;\n\t\tbuffer += stride;\n\t}\n\tfor (;i<FONT_MANAGER_GLYPHSIZE;i++) {\n\t\tmemset(buffer, 0, FONT_MANAGER_GLYPHSIZE);\n\t\tbuffer += stride;\n\t}\n\n\tstbtt_FreeSDF(tmp, fi->userdata);\n\t\n\treturn NULL;\n}\n\nconst char *\nfont_manager_glyph(struct font_manager *F, int fontid, int codepoint, int size, struct font_glyph *g, struct font_glyph *og) {\n\tint updated = font_manager_touch(F, fontid, codepoint, g);\n\t*og = *g;\n\tif (fontid != FONT_ICON && is_space_codepoint(codepoint)){\n\t\tupdated = 1;\t// not need update\n\t\tog->w = og->h = 0;\n\t}\n\tif (fontid == FONT_ICON) {\n\t\ticon_scale(g, size);\n\t} else {\n\t\tfont_manager_scale(F, g, size);\n\t}\n\tif (updated == 0) {\n\t\tconst char * err = font_manager_update(F, fontid, codepoint, og, F->texture_buffer, FONT_MANAGER_TEXSIZE);\n\t\tif (err) {\n\t\t\treturn err;\n\t\t}\n\t}\n\treturn NULL;\n}\n\nint\nfont_manager_flush(struct font_manager *F) {\n\t// todo : atomic inc\n\tlock(F);\n\tint dirty = F->dirty;\n\t++F->version;\n\tF->dirty = 0;\n\tunlock(F);\n\treturn dirty;\n}\n\nstatic void\nfont_manager_import_unsafe(struct font_manager *F, void* fontdata, size_t sz) {\n\ttruetype_import(F->L, fontdata, sz);\n}\n\nvoid\nfont_manager_import(struct font_manager *F, void* fontdata, size_t sz) {\n\tlock(F);\n\tfont_manager_import_unsafe(F, fontdata, sz);\n\tunlock(F);\n}\n\nstatic int\nfont_manager_addfont_with_family_unsafe(struct font_manager *F, const char* family) {\n\treturn ttf_with_family(F, family);\n}\n\nint\nfont_manager_addfont_with_family(struct font_manager *F, const char* family) {\n\tlock(F);\n\tint r = font_manager_addfont_with_family_unsafe(F, family);\n\tunlock(F);\n\treturn r;\n}\n\nint\nfont_manager_enum_fontname(struct font_manager *F, int idx, char buffer[], int buf_sz) {\n\tlock(F);\n\tint r = truetype_enum(F->L, idx, buffer, buf_sz);\n\tunlock(F);\n\treturn r;\n}\n\nfloat\nfont_manager_sdf_mask(struct font_manager *F){\n\treturn (ONEDGE_VALUE) / 255.f;\n}\n\nfloat\nfont_manager_sdf_distance(struct font_manager *F, uint8_t numpixel){\n\treturn (numpixel * PIXEL_DIST_SCALE) / 255.f;\n}\n\nsize_t\nfont_manager_sizeof() {\n\treturn sizeof(struct font_manager);\n}\n\nvoid\nfont_manager_icon_init(struct font_manager *F, int n, void *data) {\n\tlock(F);\n\tF->icon_n = n;\n\tF->icon_data = (unsigned char *)data;\n\tunlock(F);\n}\n\nvoid\nfont_manager_init(struct font_manager *F, void *L) {\n\tmutex_init(F->mutex);\n\tF->version = 1;\n\tF->count = 0;\n\tF->ttf = NULL;\n\tF->L = NULL;\n\tF->dpi_perinch = 0;\n\tF->dirty = 0;\n\tF->icon_n = 0;\n\tF->icon_data = NULL;\n// init priority list\n\tint i;\n\tfor (i=0;i<FONT_MANAGER_SLOTS;i++) {\n\t\tF->priority[i].prev = i+1;\n\t\tF->priority[i].next = i-1;\n\t}\n\tint lastslot = FONT_MANAGER_SLOTS-1;\n\tF->priority[0].next = lastslot;\n\tF->priority[lastslot].prev = 0;\n\tF->list_head = lastslot;\n// init hash\n\tfor (i=0;i<FONT_MANAGER_SLOTS;i++) {\n\t\tF->slots[i].codepoint_key = -1;\n\t}\n\tfor (i=0;i<FONT_MANAGER_HASHSLOTS;i++) {\n\t\tF->hash[i] = -1;\t// empty slot\n\t}\n\tmemset(F->texture_buffer, 0, sizeof(F->texture_buffer));\n\tF->ttf = truetype_cstruct(L);\n\tF->L = L;\n}\n\nvoid*\nfont_manager_shutdown(struct font_manager *F) {\n\tlock(F);\n\tvoid *L = F->L;\n\tF->ttf = NULL;\n\tF->L = NULL;\n\tunlock(F);\n\treturn L;\n}\n"
  },
  {
    "path": "src/font_manager.h",
    "content": "#ifndef font_manager_h\n#define font_manager_h\n\n#include <stb/stb_truetype.h>\n#include <stdint.h>\n#include <stddef.h>\n#include \"font_define.h\"\n\nstruct font_manager;\n\nsize_t font_manager_sizeof();\nvoid font_manager_init(struct font_manager *, void *L);\nvoid* font_manager_shutdown(struct font_manager *);\nvoid font_manager_import(struct font_manager *F, void* fontdata, size_t sz);\n\nint font_manager_addfont_with_family(struct font_manager *F, const char* family);\nvoid font_manager_fontheight(struct font_manager *F, int fontid, int size, int *ascent, int *descent, int *lineGap);\nint font_manager_pixelsize(struct font_manager *F, int fontid, int pointsize);\nconst char* font_manager_glyph(struct font_manager *F, int fontid, int codepoint, int size, struct font_glyph *g, struct font_glyph *og);\n//int font_manager_touch(struct font_manager *, int font, int codepoint, struct font_glyph *glyph);\n//const char * font_manager_update(struct font_manager *, int font, int codepoint, struct font_glyph *glyph, uint8_t *buffer, int stride);\nint font_manager_flush(struct font_manager *);\nvoid font_manager_scale(struct font_manager *F, struct font_glyph *glyph, int size);\nint font_manager_underline(struct font_manager *F, int fontid, int size, float *underline_position, float *thickness);\nfloat font_manager_sdf_mask(struct font_manager *F);\nfloat font_manager_sdf_distance(struct font_manager *F, uint8_t numpixel);\nvoid font_manager_icon_init(struct font_manager *F, int n, void *data);\nint font_manager_enum_fontname(struct font_manager *F, int idx, char buffer[], int buf_sz);\n\n// for debug\nconst void * font_manager_texture(struct font_manager *F, int *sz);\n\n#endif //font_manager_h\n"
  },
  {
    "path": "src/font_system.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n#include <stdint.h>\n\n#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)\n\n#include <windows.h>\n#define MAX_NAME 1024\n\nstatic void *\nfree_data(void *ud, void *ptr, size_t oszie, size_t nsize) {\n\tfree(ptr);\n\treturn NULL;\n}\n\nstatic int\nlttfdata(lua_State *L) {\n\tconst char * familyName = luaL_checkstring(L, 1);\n\tWCHAR familyNameW[MAX_NAME];\n\tint n = MultiByteToWideChar(CP_UTF8,0,(const char*)familyName,-1,familyNameW,MAX_NAME);\n\tif (n == 0 || n > LF_FACESIZE)\n\t\treturn luaL_error(L, \"Invalid family name %s\", familyName);\n\tHDC hdc = CreateCompatibleDC(0);\n\tLOGFONTW lf;\n\tmemset(&lf, 0, sizeof(LOGFONT));\n\tmemcpy(lf.lfFaceName, familyNameW, n * sizeof(WCHAR));\n\tlf.lfCharSet = DEFAULT_CHARSET;\n\tHFONT hfont = CreateFontIndirectW(&lf); \n\tif (!hfont) {\n\t\tDeleteDC(hdc);\n\t\treturn luaL_error(L, \"Create font failed: %d\", GetLastError());\n\t}\n    HGDIOBJ oldobj = SelectObject(hdc, hfont);\n\tuint32_t tags[2] = {0x66637474/*ttcf*/, 0};\n\tint i;\n\tDWORD bytes = 0;\n\tchar *buf = NULL;\n\tfor (i=0;i<2;i++) {\n\t\tuint32_t tag = tags[i];\n\t\tbytes = GetFontData(hdc, tag, 0, 0, 0);\n        if (bytes != GDI_ERROR) {\n\t\t\tbuf = malloc(bytes+1);//lua_newuserdatauv(L, bytes, 0);\n\t\t\tif (buf == NULL)\n\t\t\t\treturn luaL_error(L, \"Out of memory : sysfont\");\n\t\t\tbuf[bytes] = 0;\n\t\t\tbytes = GetFontData(hdc, tag, 0, (void *)buf, bytes);\n\t\t\tif (bytes != GDI_ERROR) {\n\t\t\t\tbreak;\n\t\t\t} else {\n\t\t\t\tfree(buf);\n\t\t\t\tbytes = 0;\n\t\t\t}\n\t\t}\n\t}\n\tSelectObject(hdc, oldobj);\n\tDeleteObject(hfont);\n\tDeleteDC(hdc);\n\tif (bytes == 0) {\n\t\treturn luaL_error(L, \"Read font data failed\");\n\t}\n\tlua_pushexternalstring(L, buf, bytes, free_data , NULL);\n\treturn 1;\n}\n\n#elif defined(__APPLE__)\n\n#include <CoreFoundation/CoreFoundation.h>\n#include <CoreText/CoreText.h>\n\n#define kCTFontTableTtcf 'ttcf'\n\nstatic void *\nfree_data(void *ud, void *ptr, size_t oszie, size_t nsize) {\n\tfree(ptr);\n\treturn NULL;\n}\n\nstatic CFDataRef read_font_file_data(CFURLRef url) {\n\tCFReadStreamRef stream = CFReadStreamCreateWithFile(kCFAllocatorDefault, url);\n\tif (!stream) {\n\t\treturn NULL;\n\t}\n\t\n\tif (!CFReadStreamOpen(stream)) {\n\t\tCFRelease(stream);\n\t\treturn NULL;\n\t}\n\t\n\tCFMutableDataRef data = CFDataCreateMutable(kCFAllocatorDefault, 0);\n\tUInt8 buffer[8192];\n\tCFIndex bytesRead;\n\t\n\twhile ((bytesRead = CFReadStreamRead(stream, buffer, sizeof(buffer))) > 0) {\n\t\tCFDataAppendBytes(data, buffer, bytesRead);\n\t}\n\t\n\tCFReadStreamClose(stream);\n\tCFRelease(stream);\n\t\n\tif (bytesRead < 0) {\n\t\tCFRelease(data);\n\t\treturn NULL;\n\t}\n\t\n\treturn data;\n}\n\nstatic int\nlttfdata(lua_State *L) {\n\tconst char *familyName = luaL_checkstring(L, 1);\n\t\n\tCFStringRef fontNameStr = CFStringCreateWithCString(kCFAllocatorDefault, \n\t\t\t\t\t\t\t\t\t\t\t\t\t\tfamilyName, \n\t\t\t\t\t\t\t\t\t\t\t\t\t\tkCFStringEncodingUTF8);\n\tif (!fontNameStr) {\n\t\treturn luaL_error(L, \"Failed to create font name string\");\n\t}\n\t\n\tCFDictionaryRef attributes = CFDictionaryCreate(\n\t\tkCFAllocatorDefault,\n\t\t(const void**)&kCTFontFamilyNameAttribute,\n\t\t(const void**)&fontNameStr,\n\t\t1,\n\t\t&kCFTypeDictionaryKeyCallBacks,\n\t\t&kCFTypeDictionaryValueCallBacks\n\t);\n\t\n\tCTFontDescriptorRef descriptor = CTFontDescriptorCreateWithAttributes(attributes);\n\tCFRelease(attributes);\n\tCFRelease(fontNameStr);\n\t\n\tif (!descriptor) {\n\t\treturn luaL_error(L, \"Failed to create font descriptor\");\n\t}\n\t\n\tCTFontRef font = CTFontCreateWithFontDescriptor(descriptor, 12.0, NULL);\n\tCFRelease(descriptor);\n\t\n\tif (!font) {\n\t\treturn luaL_error(L, \"Failed to create font for family: %s\", familyName);\n\t}\n\t\n\tCFDataRef fontData = NULL;\n\t\n\tCFDataRef ttcfData = CTFontCopyTable(font, kCTFontTableTtcf, kCTFontTableOptionNoOptions);\n\tif (ttcfData) {\n\t\tfontData = ttcfData;\n\t} else {\n\t\tCTFontDescriptorRef fontDesc = CTFontCopyFontDescriptor(font);\n\t\tif (fontDesc) {\n\t\t\tCFURLRef fontURL = CTFontDescriptorCopyAttribute(fontDesc, kCTFontURLAttribute);\n\t\t\tif (fontURL) {\n\t\t\t\tfontData = read_font_file_data(fontURL);\n\t\t\t\tCFRelease(fontURL);\n\t\t\t}\n\t\t\tCFRelease(fontDesc);\n\t\t}\n\t}\n\t\n\tCFRelease(font);\n\t\n\tif (!fontData) {\n\t\treturn luaL_error(L, \"Failed to get font data for family: %s\", familyName);\n\t}\n\t\n\tCFIndex dataLength = CFDataGetLength(fontData);\n\tchar *buf = malloc(dataLength + 1);\n\tif (!buf) {\n\t\tCFRelease(fontData);\n\t\treturn luaL_error(L, \"Out of memory : sysfont\");\n\t}\n\t\n\tCFDataGetBytes(fontData, CFRangeMake(0, dataLength), (UInt8*)buf);\n\tbuf[dataLength] = 0;\n\tCFRelease(fontData);\n\t\n\tlua_pushexternalstring(L, buf, dataLength, free_data, NULL);\n\treturn 1;\n}\n\n#elif defined(__EMSCRIPTEN__)\n\nstatic int\nlttfdata(lua_State *L) {\n\t// todo :\n\treturn 0;\n}\n\n#elif defined(__linux__)\n\n#include <fontconfig/fontconfig.h>\n#include <stdio.h>\n#include <stdlib.h>\n\nstatic void *\nfree_data(void *ud, void *ptr, size_t oszie, size_t nsize) {\n\tfree(ptr);\n\treturn NULL;\n}\n\nstatic int\nlttfdata(lua_State *L) {\n\tconst char *familyName = luaL_checkstring(L, 1);\n\n\tif (!FcInit()) {\n\t\treturn luaL_error(L, \"Failed to initialize fontconfig\");\n\t}\n\n\tFcPattern *pattern = FcNameParse((const FcChar8*)familyName);\n\tif (!pattern) {\n\t\tFcFini();\n\t\treturn luaL_error(L, \"Failed to parse font name: %s\", familyName);\n\t}\n\n\tFcConfigSubstitute(NULL, pattern, FcMatchPattern);\n\tFcDefaultSubstitute(pattern);\n\n\tFcResult result;\n\tFcPattern *match = FcFontMatch(NULL, pattern, &result);\n\tFcPatternDestroy(pattern);\n\n\tif (!match || result != FcResultMatch) {\n\t\tif (match) FcPatternDestroy(match);\n\t\tFcFini();\n\t\treturn luaL_error(L, \"Font not found: %s\", familyName);\n\t}\n\n\tFcChar8 *filename;\n\tif (FcPatternGetString(match, FC_FILE, 0, &filename) != FcResultMatch) {\n\t\tFcPatternDestroy(match);\n\t\tFcFini();\n\t\treturn luaL_error(L, \"Failed to get font file path for: %s\", familyName);\n\t}\n\n\tFILE *file = fopen((const char*)filename, \"rb\");\n\tif (!file) {\n\t\tFcPatternDestroy(match);\n\t\tFcFini();\n\t\treturn luaL_error(L, \"Failed to open font file: %s\", filename);\n\t}\n\n\tfseek(file, 0, SEEK_END);\n\tlong fileSize = ftell(file);\n\tfseek(file, 0, SEEK_SET);\n\n\tif (fileSize <= 0) {\n\t\tfclose(file);\n\t\tFcPatternDestroy(match);\n\t\tFcFini();\n\t\treturn luaL_error(L, \"Invalid font file size: %s\", filename);\n\t}\n\n\tchar *buf = malloc(fileSize + 1);\n\tif (!buf) {\n\t\tfclose(file);\n\t\tFcPatternDestroy(match);\n\t\tFcFini();\n\t\treturn luaL_error(L, \"Out of memory : sysfont\");\n\t}\n\n\tsize_t bytesRead = fread(buf, 1, fileSize, file);\n\tfclose(file);\n\tFcPatternDestroy(match);\n\tFcFini();\n\n\tif (bytesRead != fileSize) {\n\t\tfree(buf);\n\t\treturn luaL_error(L, \"Failed to read font file: %s\", filename);\n\t}\n\n\tbuf[fileSize] = 0;\n\n\tlua_pushexternalstring(L, buf, fileSize, free_data, NULL);\n\treturn 1;\n}\n\n#else\n\nstatic int\nlttfdata(lua_State *L) {\n\t// always failed\n\treturn 0;\n}\n\n#endif\n\nint\nluaopen_font_system(lua_State *L) {\n\tluaL_Reg l[] = {\n\t\t{ \"ttfdata\", lttfdata },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\treturn 1;\n}\n\n"
  },
  {
    "path": "src/gamepad.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n#include <stdint.h>\n#include <string.h>\n#include \"mutex.h\"\n\n// It's the same with XINPUT_GAMEAD\nstatic const char * gamepad_buttons[] = {\n\t\"UP\",\n\t\"DOWN\",\n\t\"LEFT\",\n\t\"RIGHT\",\n\t\"START\",\n\t\"BACK\",\n\t\"LS\",\n\t\"RS\",\n\t\"LB\",\n\t\"RB\",\n\t\"U0\",\t// Undef\n\t\"U1\",\t// undef\n\t\"A\",\n\t\"B\",\n\t\"X\",\n\t\"Y\",\n};\n\n#define BUTTON_COUNT (sizeof(gamepad_buttons)/sizeof(gamepad_buttons[0]))\n\nstruct gamepad_state {\n\tuint32_t packet;\n\tuint16_t buttons;\n\tuint8_t lt;\n\tuint8_t rt;\n\tint16_t ls_x;\n\tint16_t ls_y;\n\tint16_t rs_x;\n\tint16_t rs_y;\n};\n\n#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)\n\n#include <windows.h>\n#include <xinput.h>\n\nstatic int\ngamepad_getstate(int index, struct gamepad_state *state) {\n\tXINPUT_STATE * result = (XINPUT_STATE *)state;\n\tDWORD err = XInputGetState(index, result);\n\tif (err == ERROR_SUCCESS)\n\t\treturn 0;\n\t// should be ERROR_DEVICE_NOT_CONNECTED\n\treturn 1;\n}\n\n#elif defined(__APPLE__)\n\n#include <IOKit/hid/IOHIDManager.h>\n#include <IOKit/hid/IOHIDDevice.h>\n#include <CoreFoundation/CoreFoundation.h>\n\nstatic IOHIDManagerRef hid_manager = NULL;\nstatic CFMutableArrayRef gamepad_devices = NULL;\n\nstatic void gamepad_init() {\n    if (hid_manager != NULL) return;\n\n    hid_manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);\n    gamepad_devices = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);\n\n    CFMutableDictionaryRef matching = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, \n        &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);\n\n    int usage_page = kHIDPage_GenericDesktop;\n    int usage = kHIDUsage_GD_GamePad;\n\n    CFNumberRef page_ref = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage_page);\n    CFNumberRef usage_ref = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage);\n\n    CFDictionarySetValue(matching, CFSTR(kIOHIDDeviceUsagePageKey), page_ref);\n    CFDictionarySetValue(matching, CFSTR(kIOHIDDeviceUsageKey), usage_ref);\n\n    IOHIDManagerSetDeviceMatching(hid_manager, matching);\n    IOHIDManagerOpen(hid_manager, kIOHIDOptionsTypeNone);\n\n    CFSetRef device_set = IOHIDManagerCopyDevices(hid_manager);\n    if (device_set) {\n        CFIndex count = CFSetGetCount(device_set);\n        IOHIDDeviceRef *devices = malloc(count * sizeof(IOHIDDeviceRef));\n        CFSetGetValues(device_set, (const void**)devices);\n\n        for (CFIndex i = 0; i < count; i++) {\n            CFArrayAppendValue(gamepad_devices, devices[i]);\n        }\n\n        free(devices);\n        CFRelease(device_set);\n    }\n\n    CFRelease(page_ref);\n    CFRelease(usage_ref);\n    CFRelease(matching);\n}\n\nstatic int\ngamepad_getstate(int index, struct gamepad_state *state) {\n    gamepad_init();\n    \n    memset(state, 0, sizeof(struct gamepad_state));\n    \n    if (index < 0 || index >= CFArrayGetCount(gamepad_devices)) {\n        return 1;\n    }\n    \n    IOHIDDeviceRef device = (IOHIDDeviceRef)CFArrayGetValueAtIndex(gamepad_devices, index);\n    if (!device) {\n        return 1;\n    }\n    \n    CFArrayRef elements = IOHIDDeviceCopyMatchingElements(device, NULL, kIOHIDOptionsTypeNone);\n    if (!elements) {\n        return 1;\n    }\n    \n    CFIndex element_count = CFArrayGetCount(elements);\n    \n    for (CFIndex i = 0; i < element_count; i++) {\n        IOHIDElementRef element = (IOHIDElementRef)CFArrayGetValueAtIndex(elements, i);\n        IOHIDElementType type = IOHIDElementGetType(element);\n        \n        if (type == kIOHIDElementTypeInput_Button || \n            type == kIOHIDElementTypeInput_Axis) {\n            \n            IOHIDValueRef value_ref;\n            if (IOHIDDeviceGetValue(device, element, &value_ref) == kIOReturnSuccess) {\n                CFIndex value = IOHIDValueGetIntegerValue(value_ref);\n                uint32_t usage_page = IOHIDElementGetUsagePage(element);\n                uint32_t usage = IOHIDElementGetUsage(element);\n                \n                if (usage_page == kHIDPage_Button) {\n                    if (usage >= 1 && usage <= 16) {\n                        if (value) {\n                            state->buttons |= (1 << (usage - 1));\n                        }\n                    }\n                }\n                else if (usage_page == kHIDPage_GenericDesktop) {\n                    CFIndex min = IOHIDElementGetLogicalMin(element);\n                    CFIndex max = IOHIDElementGetLogicalMax(element);\n                    \n                    int16_t normalized = (int16_t)(((value - min) * 65535) / (max - min) - 32768);\n                    \n                    switch (usage) {\n                        case kHIDUsage_GD_X:\n                            state->ls_x = normalized;\n                            break;\n                        case kHIDUsage_GD_Y:\n                            state->ls_y = -normalized;\n                            break;\n                        case kHIDUsage_GD_Z:\n                            state->rs_x = normalized;\n                            break;\n                        case kHIDUsage_GD_Rz:\n                            state->rs_y = -normalized;\n                            break;\n                        case kHIDUsage_GD_Rx:\n                            state->lt = (uint8_t)((normalized + 32768) >> 8);\n                            break;\n                        case kHIDUsage_GD_Ry:\n                            state->rt = (uint8_t)((normalized + 32768) >> 8);\n                            break;\n                    }\n                }\n            }\n        }\n    }\n    \n    CFRelease(elements);\n    \n    static uint32_t packet_counter = 0;\n    state->packet = ++packet_counter;\n    \n    return 0;\n}\n\n#elif defined(__linux__)\n\n#include <fcntl.h>\n#include <unistd.h>\n#include <linux/joystick.h>\n#include <sys/ioctl.h>\n\nstatic int\ngamepad_getstate(int index, struct gamepad_state *state) {\n    char device_path[32];\n    snprintf(device_path, sizeof(device_path), \"/dev/input/js%d\", index);\n\n    int fd = open(device_path, O_RDONLY | O_NONBLOCK);\n    if (fd < 0) {\n        return 1;\n    }\n\n    static struct {\n        uint16_t buttons;\n        uint8_t lt, rt;\n        int16_t ls_x, ls_y, rs_x, rs_y;\n        uint32_t packet;\n    } cache = {0};\n\n    struct js_event event;\n\n    while (read(fd, &event, sizeof(event)) == sizeof(event)) {\n        cache.packet++;\n\n        if (event.type & JS_EVENT_BUTTON) {\n            if (event.number < 16) {\n                if (event.value) {\n                    cache.buttons |= (1 << event.number);\n                } else {\n                    cache.buttons &= ~(1 << event.number);\n                }\n            }\n        } else if (event.type & JS_EVENT_AXIS) {\n            int16_t value = event.value;\n            switch (event.number) {\n                case 0: cache.ls_x = value; break;\n                case 1: cache.ls_y = -value; break;\n                case 2: cache.lt = (value + 32768) >> 8; break;\n                case 3: cache.rs_x = value; break;\n                case 4: cache.rs_y = -value; break;\n                case 5: cache.rt = (value + 32768) >> 8; break;\n            }\n        }\n    }\n\n    close(fd);\n\n    state->packet = cache.packet;\n    state->buttons = cache.buttons;\n    state->lt = cache.lt;\n    state->rt = cache.rt;\n    state->ls_x = cache.ls_x;\n    state->ls_y = cache.ls_y;\n    state->rs_x = cache.rs_x;\n    state->rs_y = cache.rs_y;\n\n    return 0;\n}\n\n#elif defined(__EMSCRIPTEN__)\n\nstatic int\ngamepad_getstate(int index, struct gamepad_state *state) {\n\t(void)index;\n\t(void)state;\n\treturn 1;\n}\n\n#else\n\n// todo : linux and mac support\n#error Unsupport gamepad\n\n#endif\n\n#define MAX_GAMEPAD 4\n\nstatic struct gamepad_global {\n\tmutex_t lock[MAX_GAMEPAD];\n\tint connected[MAX_GAMEPAD];\n\tuint32_t packet[MAX_GAMEPAD];\n\tstruct gamepad_state state[MAX_GAMEPAD];\n} GAMEPAD;\n\nstruct gamepad_local {\n\tint connected;\n\tstruct gamepad_state state;\n};\n\nstatic int\nlgamepad_update(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TTABLE);\n\tint index = luaL_optinteger(L, 2, 0);\n\tif (index < 0 || index >= MAX_GAMEPAD) {\n\t\treturn luaL_error(L, \"Invalid gamepad id %d\", index);\n\t}\n\tstruct gamepad_local * last_state = (struct gamepad_local *)lua_touserdata(L, lua_upvalueindex(1));\n\tlast_state += index;\n\tstruct gamepad_state tmp;\n\tmutex_acquire(GAMEPAD.lock[index]);\n\tint connected = GAMEPAD.connected[index];\n\tif (connected) {\n\t\ttmp = GAMEPAD.state[index];\n\t}\n\tmutex_release(GAMEPAD.lock[index]);\n\tint i;\n\tif (connected != last_state->connected) {\n\t\tif (connected) {\n\t\t\t// reconnected, set all states\n\t\t\tlast_state->state = tmp;\n\t\t\tlua_pushinteger(L, tmp.lt);\n\t\t\tlua_setfield(L, 1, \"LT\");\n\t\t\tlua_pushinteger(L, tmp.rt);\n\t\t\tlua_setfield(L, 1, \"RT\");\n\t\t\tlua_pushinteger(L, tmp.ls_x);\n\t\t\tlua_setfield(L, 1, \"LS_X\");\n\t\t\tlua_pushinteger(L, tmp.ls_y);\n\t\t\tlua_setfield(L, 1, \"LS_Y\");\n\t\t\tlua_pushinteger(L, tmp.rs_x);\n\t\t\tlua_setfield(L, 1, \"RS_X\");\n\t\t\tlua_pushinteger(L, tmp.rs_y);\n\t\t\tuint16_t mask = tmp.buttons;\n\t\t\tfor (i=0;i<BUTTON_COUNT;i++) {\n\t\t\t\tlua_pushboolean(L, mask & 1);\n\t\t\t\tlua_setfield(L, 1, gamepad_buttons[i]);\n\t\t\t\tmask >>= 1;\n\t\t\t}\n\t\t} else {\n\t\t\t// disconnected, clear all states\n\t\t\tmemset(&last_state->state, 0, sizeof(last_state->state));\n\t\t\tlua_pushinteger(L, 0);\n\t\t\tlua_setfield(L, 1, \"LT\");\n\t\t\tlua_pushinteger(L, 0);\n\t\t\tlua_setfield(L, 1, \"RT\");\n\t\t\tlua_pushinteger(L, 0);\n\t\t\tlua_setfield(L, 1, \"LS_X\");\n\t\t\tlua_pushinteger(L, 0);\n\t\t\tlua_setfield(L, 1, \"LS_Y\");\n\t\t\tlua_pushinteger(L, 0);\n\t\t\tlua_setfield(L, 1, \"RS_X\");\n\t\t\tfor (i=0;i<BUTTON_COUNT;i++) {\n\t\t\t\tlua_pushboolean(L, 0);\n\t\t\t\tlua_setfield(L, 1, gamepad_buttons[i]);\n\t\t\t}\n\t\t}\n\t} else if (connected) {\n\t\t// state change\n\t\tstruct gamepad_state *state = &last_state->state;\n\t\tif (tmp.lt != state->lt) {\n\t\t\tstate->lt = tmp.lt;\n\t\t\tlua_pushinteger(L, tmp.lt);\n\t\t\tlua_setfield(L, 1, \"LT\");\n\t\t}\n\t\tif (tmp.rt != state->rt) {\n\t\t\tstate->rt = tmp.rt;\n\t\t\tlua_pushinteger(L, tmp.rt);\n\t\t\tlua_setfield(L, 1, \"RT\");\n\t\t}\n\t\tif (tmp.ls_x != state->ls_x) {\n\t\t\tstate->ls_x = tmp.ls_x;\n\t\t\tlua_pushinteger(L, tmp.ls_x);\n\t\t\tlua_setfield(L, 1, \"LS_X\");\n\t\t}\n\t\tif (tmp.ls_y != state->ls_y) {\n\t\t\tstate->ls_y = tmp.ls_y;\n\t\t\tlua_pushinteger(L, tmp.ls_y);\n\t\t\tlua_setfield(L, 1, \"LS_Y\");\n\t\t}\n\t\tif (tmp.rs_x != state->rs_x) {\n\t\t\tstate->rs_x = tmp.rs_x;\n\t\t\tlua_pushinteger(L, tmp.rs_x);\n\t\t\tlua_setfield(L, 1, \"RS_X\");\n\t\t}\n\t\tif (tmp.rs_y != state->rs_y) {\n\t\t\tstate->rs_y = tmp.rs_y;\n\t\t\tlua_pushinteger(L, tmp.rs_y);\n\t\t\tlua_setfield(L, 1, \"RS_Y\");\n\t\t}\n\t\tuint16_t mask = tmp.buttons;\n\t\tuint16_t last_mask = state->buttons;\n\t\tif (mask != last_mask) {\n\t\t\tfor (i=0;i<BUTTON_COUNT;i++) {\n\t\t\t\tint press = mask & 1;\n\t\t\t\tif (press != (last_mask & 1)) {\n\t\t\t\t\tlua_pushboolean(L, press);\n\t\t\t\t\tlua_setfield(L, 1, gamepad_buttons[i]);\n\t\t\t\t}\n\t\t\t\tmask >>= 1;\n\t\t\t\tlast_mask >>= 1;\n\t\t\t}\n\t\t\tstate->buttons = tmp.buttons;\n\t\t}\n\t}\n\tlua_pushboolean(L, connected);\n\tlua_setfield(L, 1, \"connect\");\n\tlast_state->connected = connected;\n\tlua_pushboolean(L, connected);\n\treturn 1;\n}\n\nint\nluaopen_gamepad(lua_State *L) {\n\tluaL_checkversion(L);\n\tlua_newtable(L);\n\tstruct gamepad_local * state = (struct gamepad_local *)lua_newuserdatauv(L, sizeof(*state) * MAX_GAMEPAD, 0);\n\tmemset(state, 0, sizeof(*state) * MAX_GAMEPAD);\n\tlua_pushcclosure(L, lgamepad_update, 1);\n\tlua_setfield(L, -2, \"update\");\n\treturn 1;\n}\n\nstatic int\nlgamepad_device_init(lua_State *L) {\n\tint i;\n\tmemset(&GAMEPAD, 0, sizeof(GAMEPAD));\n\t\n\tfor (i=0;i<MAX_GAMEPAD;i++) {\n\t\tmutex_init(GAMEPAD.lock[i]);\n\t}\n\treturn 0;\n}\n\nstatic int\nlgamepad_device_deinit(lua_State *L) {\n\tmemset(&GAMEPAD, 0, sizeof(GAMEPAD));\n\treturn 0;\n}\n\nstatic int\nlgamepad_device_update(lua_State *L) {\n\tint i;\n\tint changed = 0;\n\tfor (i=0;i<MAX_GAMEPAD;i++) {\n\t\tmutex_acquire(GAMEPAD.lock[i]);\n\t\t\tint err = gamepad_getstate(i, &GAMEPAD.state[i]);\n\t\t\tif (err) {\n\t\t\t\t// disconnected\n\t\t\t\tif (GAMEPAD.connected[i]) {\n\t\t\t\t\tGAMEPAD.connected[i] = 0;\n\t\t\t\t\tchanged = 1;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// connected\n\t\t\t\tif (GAMEPAD.connected[i]) {\n\t\t\t\t\tif (GAMEPAD.packet[i] != GAMEPAD.state[i].packet) {\n\t\t\t\t\t\tchanged = 1;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tGAMEPAD.connected[i] = 1;\n\t\t\t\t\tchanged = 1;\n\t\t\t\t}\n\t\t\t\tGAMEPAD.packet[i] = GAMEPAD.state[i].packet;\n\t\t\t}\n\t\tmutex_release(GAMEPAD.lock[i]);\n\t}\n\tlua_pushboolean(L, changed);\n\treturn 1;\n}\n\nint\nluaopen_gamepad_device(lua_State *L) {\n\tluaL_checkversion(L);\n\tluaL_Reg l[] = {\n\t\t{ \"init\", lgamepad_device_init },\n\t\t{ \"deinit\", lgamepad_device_deinit },\n\t\t{ \"update\", lgamepad_device_update },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\treturn 1;\n}\n"
  },
  {
    "path": "src/image.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n\n#include <stdint.h>\n#include <string.h>\n#include <stdlib.h>\n\nstatic void *\nmalloc_luastring(size_t sz) {\n\tunsigned char * buffer = malloc(sz+1);\n\tif (buffer) {\n\t\tbuffer[sz] = 0;\n\t\treturn (void *)buffer;\n\t} else {\n\t\treturn NULL;\n\t}\n}\n\nstatic void *\nrealloc_luastring(void *ptr, size_t sz) {\n\tunsigned char * buffer = realloc(ptr, sz+1);\n\tif (buffer) {\n\t\tbuffer[sz] = 0;\n\t\treturn (void *)buffer;\n\t} else {\n\t\treturn NULL;\n\t}\n}\n\n#define STBI_MALLOC malloc_luastring\n#define STBI_FREE free\n#define STBI_REALLOC realloc_luastring\n\n#define STBI_ONLY_PNG\n#define STBI_MAX_DIMENSIONS 65536\n#define STBI_NO_STDIO\n#define STB_IMAGE_IMPLEMENTATION\n#include \"stb/stb_image.h\"\n\n#define STBIW_WINDOWS_UTF8\n#define STB_IMAGE_WRITE_IMPLEMENTATION\n#include \"stb/stb_image_write.h\"\n\n#include \"stb/stb_image_resize2.h\"\n\n#include \"luabuffer.h\"\n\nstatic void *\nfree_image(void *ud, void *ptr, size_t osize, size_t nsize) {\n\tstbi_image_free(ptr);\n\treturn NULL;\n}\n\nstatic void *\nfree_buffer(void *ud, void *ptr, size_t osize, size_t nsize) {\n\tfree(ptr);\n\treturn NULL;\n}\n\nstatic int\nload_image(lua_State *L, int *x, int *y, stbi_uc **output) {\n\tsize_t sz;\n\tint c;\n\tconst stbi_uc *buffer = luaL_getbuffer(L, &sz);\n\tstbi_uc * img = stbi_load_from_memory(buffer, sz, x, y, &c, 4);\n\tif (img == NULL) {\n\t\tlua_pushnil(L);\n\t\tlua_pushstring(L, stbi_failure_reason());\n\t\treturn 2;\n\t}\n\t*output = img;\n\treturn 0;\n}\n\nstatic int\nimage_load(lua_State *L) {\n\tint x, y;\n\tstbi_uc * img = NULL;\n\tint r = load_image(L, &x, &y, &img);\n\tif (r)\n\t\treturn r;\n\tlua_pushexternalstring(L, (const char *)img, x * y * 4, free_image, NULL);\n\tlua_pushinteger(L, x);\n\tlua_pushinteger(L, y);\n\treturn 3;\n}\n\nstatic int\nimage_load_alpha(lua_State *L) {\n\tint x, y;\n\tstbi_uc * img = NULL;\n\tint r = load_image(L, &x, &y, &img);\n\tif (r)\n\t\treturn r;\n\tint i, j;\n\tstbi_uc * ptr = img;\n\tfor (i=0;i<y;i++) {\n\t\tfor (j=0;j<x;j++) {\n\t\t\tptr[3] = 255 - ptr[0];\n\t\t\tptr[0] = 0;\n\t\t\tptr[1] = 0;\n\t\t\tptr[2] = 0;\n\t\t\tptr += 4;\n\t\t}\n\t}\n\tlua_pushexternalstring(L, (const char *)img, x * y * 4, free_image, NULL);\n\tlua_pushinteger(L, x);\n\tlua_pushinteger(L, y);\n\treturn 3;\n};\n\nstatic int\nimage_info(lua_State *L) {\n\tsize_t sz;\n\tconst stbi_uc* buffer = luaL_getbuffer(L, &sz);\n\tint x, y, c;\n\tif (!stbi_info_from_memory(buffer, sz, &x, &y, &c)) {\n\t\tlua_pushnil(L);\n\t\tlua_pushstring(L, stbi_failure_reason());\n\t\treturn 2;\n\t}\n\tlua_pushinteger(L, x);\n\tlua_pushinteger(L, y);\n\tlua_pushinteger(L, c);\n\treturn 3;\n}\n\nstruct rect {\n\tconst uint8_t * ptr;\n\tint stride;\n\tint width;\n\tint line;\n};\n\nstatic int\nrect_init(struct rect *r, const uint8_t *buffer, int x, int y, int dx, int dy, int w, int h) {\n\tif (dx < 0) {\n\t\tw += dx;\n\t\tdx = 0;\n\t} else if (dx > x) {\n\t\treturn 0;\n\t}\n\tif (dy < 0) {\n\t\th += dy;\n\t\tdy = 0;\n\t} else if (dy > y) {\n\t\treturn 0;\n\t}\n\tif (w + dx > x) {\n\t\tw = x - dx;\n\t}\n\tif (h + dy > y) {\n\t\th = y - dy;\n\t}\n\tif (w <=0 || h <= 0)\n\t\treturn 0;\n\t\n\tr->ptr = buffer + x * 4 * dy + 4 * dx;\n\tr->stride = 4 * x;\n\tr->width = w;\n\tr->line = h;\n\n\treturn 1;\n}\n\nstatic int\nremove_top(struct rect *r) {\n\tint x, y;\n\tconst uint8_t * ptr = r->ptr;\n\tfor (y=0;y<r->line;y++) {\n\t\tconst uint8_t *cur = ptr;\n\t\tfor (x=0;x<r->width;x++) {\n\t\t\tif (cur[3]) {\n\t\t\t\tr->ptr = ptr;\n\t\t\t\tr->line -= y;\n\t\t\t\treturn y;\n\t\t\t}\n\t\t\tcur += 4;\n\t\t}\n\t\tptr += r->stride;\n\t}\n\treturn r->line;\n}\n\nstatic int\nremove_bottom(struct rect *r) {\n\tint x, y;\n\tconst uint8_t * ptr = r->ptr + (r->line - 1) * r->stride;\n\tfor (y=0;y<r->line-1;y++) {\n\t\tconst uint8_t *cur = ptr;\n\t\tfor (x=0;x<r->width;x++) {\n\t\t\tif (cur[3]) {\n\t\t\t\tr->line -= y;\n\t\t\t\treturn y;\n\t\t\t}\n\t\t\tcur += 4;\n\t\t}\n\t\tptr -= r->stride;\n\t}\n\treturn r->line;\n}\n\nstatic int\nremove_left(struct rect *r) {\n\tint x, y;\n\tconst uint8_t * ptr = r->ptr;\n\tint min_left = r->width;\n\tfor (y=0;y<r->line;y++) {\n\t\tconst uint8_t *cur = ptr;\n\t\tfor (x=0;x<min_left;x++) {\n\t\t\tif (cur[3]) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcur += 4;\n\t\t}\n\t\tif (x == 0)\n\t\t\treturn 0;\n\t\telse if (x < min_left) {\n\t\t\tmin_left = x;\n\t\t}\n\t\tptr += r->stride;\n\t}\n\treturn min_left;\n}\n\nstatic int\nremove_right(struct rect *r) {\n\tint x, y;\n\tconst uint8_t * ptr = r->ptr + r->width * 4;\n\tint min_right = r->width;\n\tfor (y=0;y<r->line;y++) {\n\t\tconst uint8_t *cur = ptr;\n\t\tfor (x=0;x<min_right;x++) {\n\t\t\tcur -= 4;\n\t\t\tif (cur[3]) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (x == 0)\n\t\t\treturn 0;\n\t\telse if (x < min_right) {\n\t\t\tmin_right = x;\n\t\t}\n\t\tptr += r->stride;\n\t}\n\treturn min_right;\n}\n\nstatic int\nimage_crop(lua_State *L) {\n\tsize_t sz;\n\tconst uint8_t * image = luaL_getbuffer(L, &sz);\n\tint x = luaL_checkinteger(L, 2);\n\tint y = luaL_checkinteger(L, 3);\n\tif (x * y * 4 != sz)\n\t\treturn luaL_error(L, \"Invalid image size %d * %d * 4 != %z\\n\", x, y, sz);\n\tint dx = luaL_optinteger(L, 4, 0);\n\tint dy = luaL_optinteger(L, 5, 0);\n\tint w = luaL_optinteger(L, 6, x - dx);\n\tint h = luaL_optinteger(L, 7, y - dy);\n\t\n\tstruct rect r;\n\n\tif (!(rect_init(&r, image, x, y, dx, dy, w, h))) {\n\t\treturn 0;\n\t}\n\n\tint top = remove_top(&r);\n\tif (top == h)\n\t\treturn 0;\n\tremove_bottom(&r);\n\tint left = remove_left(&r);\n\tint right = remove_right(&r);\n\t// reserve border for alpha channel\n\tlua_pushinteger(L, left);\n\tlua_pushinteger(L, top);\n\tlua_pushinteger(L, r.width - (left + right));\n\tlua_pushinteger(L, r.line);\n\n\treturn 4;\n}\n\nstatic uint8_t *\nget_image_buffer(lua_State *L, int *w, int *h) {\n\tuint8_t * buffer = lua_touserdata(L, 1);\n\tif (buffer == NULL || !lua_getmetatable(L, 1))\n\t\tluaL_error(L, \"Neet image userdata\");\n\tif (lua_getfield(L, -1, \"width\") != LUA_TNUMBER) {\n\t\tluaL_error(L, \"No .width\");\n\t}\n\tint width = lua_tointeger(L, -1);\n\tlua_pop(L, 1);\n\t\n\tif (lua_getfield(L, -1, \"height\") != LUA_TNUMBER) {\n\t\tluaL_error(L, \"No .height\");\n\t}\n\tint height = lua_tointeger(L, -1);\n\tlua_pop(L, 1);\n\t\n\tint size = lua_rawlen(L, 1);\n\tif (width * height * 4 != size)\n\t\tluaL_error(L, \"Invalid size %d * %d * 4 != %d\", width, height, size);\n\t*w = width;\n\t*h = height;\n\treturn buffer;\n}\n\nstatic int\nlimage_write_png(lua_State *L) {\n\tint width, height;\n\tuint8_t * buffer = get_image_buffer(L, &width, &height);\n\t\n\tconst char * filename = luaL_checkstring(L, 2);\n\tif (!stbi_write_png(filename, width, height, 4, buffer, width * 4)) {\n\t\treturn luaL_error(L, \"Write %s failed\", filename);\n\t}\n\n\treturn 0;\n}\n\nstruct canvas {\n\tvoid * buffer;\n\tint width;\n\tint height;\n\tint stride;\n};\n\nstatic int\nlimage_tocanvas(lua_State *L) {\n\tint width, height;\n\tuint8_t * buffer = get_image_buffer(L, &width, &height);\n\tstruct canvas * c = (struct canvas *)lua_newuserdatauv(L, sizeof(*c), 1);\n\tlua_pushvalue(L, 1);\n\tlua_setiuservalue(L, -2, 1);\n\tc->buffer = buffer;\n\tc->width = width;\n\tc->height = height;\n\tc->stride = width * 4;\n\n\treturn 1;\n}\n\nstatic int\nimage_new(lua_State *L) {\n\tint w = luaL_checkinteger(L, 1);\n\tint h = luaL_checkinteger(L, 2);\n\tuint8_t * buffer = (uint8_t *)lua_newuserdatauv(L, w * h * 4, 0);\n\t\n\tsize_t sz = 0;\n\tconst char *data = NULL;\n\tswitch (lua_type(L, 3)) {\n\tcase LUA_TSTRING:\n\t\tdata = lua_tolstring(L, 3, &sz);\n\t\tbreak;\n\tcase LUA_TLIGHTUSERDATA:\n\t\tdata = (const char *)lua_touserdata(L, 3);\n\t\tsz = luaL_checkinteger(L, 4);\n\t\tbreak;\n\tcase LUA_TUSERDATA:\n\t\tdata = (const char *)lua_touserdata(L, 3);\n\t\tsz = lua_rawlen(L, 3);\n\t\tbreak;\n\t}\n\tif (sz == 0) {\n\t\tmemset(buffer, 0, w * h * 4);\n\t} else if (sz == w*h*4) {\n\t\tmemcpy(buffer, data, sz);\n\t} else if (sz == w*h) {\n\t\tint i,j;\n\t\tuint8_t *dst = buffer;\n\t\tconst uint8_t *src = (const uint8_t *)data;\n\t\tfor (i=0;i<h;i++) {\n\t\t\tfor (j=0;j<w;j++) {\n\t\t\t\tuint8_t alpha = *src;\n\t\t\t\tdst[0] = dst[1] = dst[2] = dst[3] = alpha;\n\t\t\t\t++src;\n\t\t\t\tdst += 4;\n\t\t\t}\n\t\t}\n\t} else {\n\t\treturn luaL_error(L, \"Invalid image data size\");\n\t}\n\n\tlua_newtable(L);\n\n\tlua_pushvalue(L, -1);\n\tlua_setfield(L, -2, \"__index\");\n\n\tlua_pushvalue(L, 1);\n\tlua_setfield(L, -2, \"width\");\n\n\tlua_pushvalue(L, 2);\n\tlua_setfield(L, -2, \"height\");\n\n\tlua_pushcfunction(L, limage_write_png);\n\tlua_setfield(L, -2, \"write\");\n\n\tlua_pushcfunction(L, limage_tocanvas);\n\tlua_setfield(L, -2, \"canvas\");\n\n\tlua_setmetatable(L, -2);\n\treturn 1;\n}\n\nstatic int\nimage_canvas(lua_State *L) {\n\tint t = lua_type(L, 1);\n\tif (t != LUA_TSTRING && t != LUA_TUSERDATA && t != LUA_TLIGHTUSERDATA)\n\t\treturn luaL_error(L, \"Need buffer\");\n\tint width = luaL_checkinteger(L, 2);\n\tint height = luaL_checkinteger(L, 3);\n\tint stride;\n\tint stack_top= lua_gettop(L);\n\tint x = 0;\n\tint y = 0;\n\tif (stack_top <= 4) {\n\t\tstride = luaL_optinteger(L, 4, width * 4);\n\t}  else {\n\t\tx = luaL_checkinteger(L, 4);\n\t\ty = luaL_checkinteger(L, 5);\n\t\tint w = luaL_checkinteger(L, 6);\n\t\tint h = luaL_checkinteger(L, 7);\n\t\tstride = width * 4;\n\t\tif (x < 0 || y < 0 || x+w > width || y+h > height) {\n\t\t\treturn luaL_error(L, \"Invalid rect (%d %d %d %d) in (%d %d)\", x, y, w, h, width, height);\n\t\t}\n\t\twidth = w;\n\t\theight = h;\n\t}\n\tstruct canvas * c = (struct canvas *)lua_newuserdatauv(L, sizeof(*c), 1);\n\tlua_pushvalue(L, 1);\n\tlua_setiuservalue(L, -2, 1);\n\tif (t == LUA_TSTRING) {\n\t\tsize_t sz;\n\t\tc->buffer = (void *)lua_tolstring(L, 1, &sz);\n\t\tif (stride * (y + height) > sz)\n\t\t\treturn luaL_error(L, \"Invalid buffer size %d * %d > %d\", stride, (y + height), sz);\n\t} else {\n\t\tc->buffer = lua_touserdata(L, 1);\n\t}\n\tc->buffer = (void *)((char *)c->buffer + y * stride + x * 4);\n\tc->width = width;\n\tc->height = height;\n\tc->stride = stride;\n\treturn 1;\t\n}\n\nstatic int\ncheck_canvas(lua_State *L, int index) {\n\tif (lua_type(L, index) != LUA_TUSERDATA)\n\t\treturn luaL_error(L, \"Need canvas\");\n\t\n\tint t = lua_getiuservalue(L, index, 1);\n\tif (t != LUA_TSTRING && t != LUA_TUSERDATA && t != LUA_TLIGHTUSERDATA)\n\t\treturn luaL_error(L, \"Invalid canvas at %d\", index);\n\tlua_pop(L, 1);\n\treturn t;\n}\n\nstatic int\nimage_canvas_size(lua_State *L) {\n\tcheck_canvas(L, 1);\n\tstruct canvas * c = (struct canvas *)lua_touserdata(L, 1);\n\t\n\tlua_pushinteger(L, c->width);\n\tlua_pushinteger(L, c->height);\n\t\n\tlua_pushlightuserdata(L, c->buffer);\n\t\n\treturn 3;\n}\n\nstatic int\ncanvas_blit(lua_State *L) {\n\tif (check_canvas(L, 1) == LUA_TSTRING)\n\t\treturn luaL_error(L, \"dst canvas is readonly\");\n\tcheck_canvas(L, 2);\n\tstruct canvas * dst = (struct canvas *)lua_touserdata(L, 1);\n\tstruct canvas * src = (struct canvas *)lua_touserdata(L, 2);\n\tint x = luaL_optinteger(L, 3, 0);\n\tint y = luaL_optinteger(L, 4, 0);\n\tint w = src->width;\n\tint h = src->height;\n\tint sx = 0;\n\tint sy = 0;\n\tif (x < 0) {\n\t\tw += x;\n\t\tsx = -x;\n\t\tx = 0;\n\t}\n\tif (y < 0) {\n\t\th += y;\n\t\tsy = -y;\n\t\ty = 0;\n\t}\n\tif (x + w > dst->width) {\n\t\tw = dst->width - x;\n\t}\n\tif (y + h > dst->height) {\n\t\th = dst->height - y;\n\t}\n\tif (w <=0 || h <= 0)\n\t\treturn 0;\n\n\tint i;\n\tuint8_t * dst_ptr = (uint8_t *)dst->buffer + y * dst->stride + 4 * x;\n\tconst uint8_t *src_ptr = (const uint8_t *)src->buffer + sy * src->stride + 4 * sx;\n\tfor (i=0;i<h;i++) {\n\t\tmemcpy(dst_ptr, src_ptr, w * 4);\n\t\tsrc_ptr += src->stride;\n\t\tdst_ptr += dst->stride;\n\t}\n\t\n\treturn 0;\n}\n\nstatic int\nimage_makeindex(lua_State *L) {\n\tif (lua_isnoneornil(L, 1)) {\n\t\tlua_pushinteger(L, -1);\n\t\treturn 1;\n\t}\n\tint x = luaL_checkinteger(L, 1);\n\tint y = luaL_checkinteger(L, 2);\n\tint w = luaL_checkinteger(L, 3);\n\tint h = luaL_checkinteger(L, 4);\n\tunion {\n\t\tuint64_t index;\n\t\tuint16_t v[4];\n\t} u;\n\tu.v[0] = (uint16_t)x;\n\tu.v[1] = (uint16_t)y;\n\tu.v[2] = (uint16_t)w;\n\tu.v[3] = (uint16_t)h;\n\tlua_pushinteger(L, u.index);\n\treturn 1;\n}\n\nstatic int\nimage_resize(lua_State *L) {\n\tsize_t sz;\n\tconst char *buffer = luaL_checklstring(L, 1, &sz);\n\tint x = luaL_checkinteger(L, 2);\n\tint y = luaL_checkinteger(L, 3);\n\tfloat scale_x = luaL_checknumber(L, 4);\n\tfloat scale_y = luaL_optnumber(L, 5, scale_x);\n\tint channel;\n\tsize_t output_sz;\n\tint tx = (int)(x * scale_x + 0.5);\n\tint ty = (int)(y * scale_y + 0.5);\n\tint stride_p;\n\tif (x * y * 4 == sz) {\n\t\tchannel = STBIR_4CHANNEL;\n\t\toutput_sz =  tx * ty * 4;\n\t\tstride_p = 4;\n\t} else {\n\t\tif (x * y != sz) {\n\t\t\treturn luaL_error(L, \"Invalid size (%d) != %d * %d\", (int)sz, x, y);\n\t\t}\n\t\tchannel = STBIR_1CHANNEL;\n\t\toutput_sz = tx * ty;\n\t\tstride_p = 1;\n\t}\n\tunsigned char *output = (unsigned char *)malloc(output_sz +1);\n\tif (output == NULL)\n\t\treturn luaL_error(L, \"Out of memory\");\n\toutput[output_sz] = 0;\n\tmemcpy(output, buffer, output_sz);\n\n\tstbir_resize_uint8_linear((const unsigned char *)buffer , x , y, stride_p * x, output, tx, ty, stride_p * tx, channel);\n\tlua_pushexternalstring(L, (const char *)output, output_sz, free_buffer, NULL);\n\tlua_pushinteger(L, tx);\n\tlua_pushinteger(L, ty);\n\treturn 3;\n}\n\nint\nluaopen_image(lua_State *L) {\n\tluaL_checkversion(L);\n\tluaL_Reg l[] = {\n\t\t{ \"load\", image_load },\n\t\t{ \"load_alpha\", image_load_alpha },\n\t\t{ \"resize\", image_resize },\n\t\t{ \"info\", image_info },\n\t\t{ \"crop\", image_crop },\n\t\t{ \"canvas\", image_canvas },\n\t\t{ \"canvas_size\", image_canvas_size },\n\t\t{ \"new\", image_new },\n\t\t{ \"blit\", canvas_blit },\n\t\t{ \"makeindex\", image_makeindex },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\treturn 1;\n}\n"
  },
  {
    "path": "src/ime_char_filter.h",
    "content": "#ifndef SOLUNA_IME_CHAR_FILTER_H\n#define SOLUNA_IME_CHAR_FILTER_H\n\n#include <stdbool.h>\n#include <stdint.h>\n#include <string.h>\n\nstruct soluna_ime_char_filter_state {\n    uint32_t *expected_chars;\n    int *expected_count;\n    uint32_t *ignore_chars;\n    int *ignore_count;\n    int capacity;\n};\n\nstatic inline void\nsoluna_ime_char_queue_push(uint32_t *buffer, int *count, int max, uint32_t code) {\n    if (*count == max) {\n        memmove(buffer, buffer + 1, (size_t)(max - 1) * sizeof(uint32_t));\n        buffer[max - 1] = code;\n    } else {\n        buffer[*count] = code;\n        (*count)++;\n    }\n}\n\nstatic inline bool\nsoluna_ime_char_queue_consume(uint32_t *buffer, int *count, uint32_t code) {\n    for (int i = 0; i < *count; ++i) {\n        if (buffer[i] == code) {\n            if (i < *count - 1) {\n                memmove(buffer + i, buffer + i + 1, (size_t)(*count - i - 1) * sizeof(uint32_t));\n            }\n            (*count)--;\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic inline void\nsoluna_ime_char_filter_reset(struct soluna_ime_char_filter_state state) {\n    *state.expected_count = 0;\n    *state.ignore_count = 0;\n}\n\nstatic inline void\nsoluna_ime_char_filter_push_expected(struct soluna_ime_char_filter_state state, uint32_t code) {\n    soluna_ime_char_queue_push(state.expected_chars, state.expected_count, state.capacity, code);\n}\n\nstatic inline bool\nsoluna_ime_char_filter_should_skip(struct soluna_ime_char_filter_state state, uint32_t code) {\n    if (soluna_ime_char_queue_consume(state.expected_chars, state.expected_count, code)) {\n        soluna_ime_char_queue_push(state.ignore_chars, state.ignore_count, state.capacity, code);\n        return false;\n    }\n    if (*state.ignore_count > 0) {\n        if (soluna_ime_char_queue_consume(state.ignore_chars, state.ignore_count, code)) {\n            return true;\n        } else if (*state.ignore_count > 0) {\n            uint32_t stale = state.ignore_chars[0];\n            soluna_ime_char_queue_consume(state.ignore_chars, state.ignore_count, stale);\n        }\n    }\n    return false;\n}\n\n#endif /* SOLUNA_IME_CHAR_FILTER_H */\n"
  },
  {
    "path": "src/ime_state.h",
    "content": "#ifndef SOLUNA_IME_STATE_H\n#define SOLUNA_IME_STATE_H\n\n#include <stdbool.h>\n#include <stdint.h>\n\nstruct soluna_ime_rect_state {\n    float x;\n    float y;\n    float w;\n    float h;\n    uint32_t text_color;\n    bool valid;\n};\n\nextern struct soluna_ime_rect_state g_soluna_ime_rect;\n\n#endif /* SOLUNA_IME_STATE_H */\n"
  },
  {
    "path": "src/lcrypt.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n\n#include <time.h>\n#include <stdint.h>\n#include <string.h>\n#include <stdlib.h>\n\n#define PADDING_MODE_ISO7816_4 0\n#define PADDING_MODE_PKCS7 1\n#define PADDING_MODE_COUNT 2\n\n#define SMALL_CHUNK 256\n\n/* the eight DES S-boxes */\n\nstatic uint32_t SB1[64] = {\n\t0x01010400, 0x00000000, 0x00010000, 0x01010404,\n\t0x01010004, 0x00010404, 0x00000004, 0x00010000,\n\t0x00000400, 0x01010400, 0x01010404, 0x00000400,\n\t0x01000404, 0x01010004, 0x01000000, 0x00000004,\n\t0x00000404, 0x01000400, 0x01000400, 0x00010400,\n\t0x00010400, 0x01010000, 0x01010000, 0x01000404,\n\t0x00010004, 0x01000004, 0x01000004, 0x00010004,\n\t0x00000000, 0x00000404, 0x00010404, 0x01000000,\n\t0x00010000, 0x01010404, 0x00000004, 0x01010000,\n\t0x01010400, 0x01000000, 0x01000000, 0x00000400,\n\t0x01010004, 0x00010000, 0x00010400, 0x01000004,\n\t0x00000400, 0x00000004, 0x01000404, 0x00010404,\n\t0x01010404, 0x00010004, 0x01010000, 0x01000404,\n\t0x01000004, 0x00000404, 0x00010404, 0x01010400,\n\t0x00000404, 0x01000400, 0x01000400, 0x00000000,\n\t0x00010004, 0x00010400, 0x00000000, 0x01010004\n};\n\nstatic uint32_t SB2[64] = {\n\t0x80108020, 0x80008000, 0x00008000, 0x00108020,\n\t0x00100000, 0x00000020, 0x80100020, 0x80008020,\n\t0x80000020, 0x80108020, 0x80108000, 0x80000000,\n\t0x80008000, 0x00100000, 0x00000020, 0x80100020,\n\t0x00108000, 0x00100020, 0x80008020, 0x00000000,\n\t0x80000000, 0x00008000, 0x00108020, 0x80100000,\n\t0x00100020, 0x80000020, 0x00000000, 0x00108000,\n\t0x00008020, 0x80108000, 0x80100000, 0x00008020,\n\t0x00000000, 0x00108020, 0x80100020, 0x00100000,\n\t0x80008020, 0x80100000, 0x80108000, 0x00008000,\n\t0x80100000, 0x80008000, 0x00000020, 0x80108020,\n\t0x00108020, 0x00000020, 0x00008000, 0x80000000,\n\t0x00008020, 0x80108000, 0x00100000, 0x80000020,\n\t0x00100020, 0x80008020, 0x80000020, 0x00100020,\n\t0x00108000, 0x00000000, 0x80008000, 0x00008020,\n\t0x80000000, 0x80100020, 0x80108020, 0x00108000\n};\n\nstatic uint32_t SB3[64] = {\n\t0x00000208, 0x08020200, 0x00000000, 0x08020008,\n\t0x08000200, 0x00000000, 0x00020208, 0x08000200,\n\t0x00020008, 0x08000008, 0x08000008, 0x00020000,\n\t0x08020208, 0x00020008, 0x08020000, 0x00000208,\n\t0x08000000, 0x00000008, 0x08020200, 0x00000200,\n\t0x00020200, 0x08020000, 0x08020008, 0x00020208,\n\t0x08000208, 0x00020200, 0x00020000, 0x08000208,\n\t0x00000008, 0x08020208, 0x00000200, 0x08000000,\n\t0x08020200, 0x08000000, 0x00020008, 0x00000208,\n\t0x00020000, 0x08020200, 0x08000200, 0x00000000,\n\t0x00000200, 0x00020008, 0x08020208, 0x08000200,\n\t0x08000008, 0x00000200, 0x00000000, 0x08020008,\n\t0x08000208, 0x00020000, 0x08000000, 0x08020208,\n\t0x00000008, 0x00020208, 0x00020200, 0x08000008,\n\t0x08020000, 0x08000208, 0x00000208, 0x08020000,\n\t0x00020208, 0x00000008, 0x08020008, 0x00020200\n};\n\nstatic uint32_t SB4[64] = {\n\t0x00802001, 0x00002081, 0x00002081, 0x00000080,\n\t0x00802080, 0x00800081, 0x00800001, 0x00002001,\n\t0x00000000, 0x00802000, 0x00802000, 0x00802081,\n\t0x00000081, 0x00000000, 0x00800080, 0x00800001,\n\t0x00000001, 0x00002000, 0x00800000, 0x00802001,\n\t0x00000080, 0x00800000, 0x00002001, 0x00002080,\n\t0x00800081, 0x00000001, 0x00002080, 0x00800080,\n\t0x00002000, 0x00802080, 0x00802081, 0x00000081,\n\t0x00800080, 0x00800001, 0x00802000, 0x00802081,\n\t0x00000081, 0x00000000, 0x00000000, 0x00802000,\n\t0x00002080, 0x00800080, 0x00800081, 0x00000001,\n\t0x00802001, 0x00002081, 0x00002081, 0x00000080,\n\t0x00802081, 0x00000081, 0x00000001, 0x00002000,\n\t0x00800001, 0x00002001, 0x00802080, 0x00800081,\n\t0x00002001, 0x00002080, 0x00800000, 0x00802001,\n\t0x00000080, 0x00800000, 0x00002000, 0x00802080\n};\n\nstatic uint32_t SB5[64] = {\n\t0x00000100, 0x02080100, 0x02080000, 0x42000100,\n\t0x00080000, 0x00000100, 0x40000000, 0x02080000,\n\t0x40080100, 0x00080000, 0x02000100, 0x40080100,\n\t0x42000100, 0x42080000, 0x00080100, 0x40000000,\n\t0x02000000, 0x40080000, 0x40080000, 0x00000000,\n\t0x40000100, 0x42080100, 0x42080100, 0x02000100,\n\t0x42080000, 0x40000100, 0x00000000, 0x42000000,\n\t0x02080100, 0x02000000, 0x42000000, 0x00080100,\n\t0x00080000, 0x42000100, 0x00000100, 0x02000000,\n\t0x40000000, 0x02080000, 0x42000100, 0x40080100,\n\t0x02000100, 0x40000000, 0x42080000, 0x02080100,\n\t0x40080100, 0x00000100, 0x02000000, 0x42080000,\n\t0x42080100, 0x00080100, 0x42000000, 0x42080100,\n\t0x02080000, 0x00000000, 0x40080000, 0x42000000,\n\t0x00080100, 0x02000100, 0x40000100, 0x00080000,\n\t0x00000000, 0x40080000, 0x02080100, 0x40000100\n};\n\nstatic uint32_t SB6[64] = {\n\t0x20000010, 0x20400000, 0x00004000, 0x20404010,\n\t0x20400000, 0x00000010, 0x20404010, 0x00400000,\n\t0x20004000, 0x00404010, 0x00400000, 0x20000010,\n\t0x00400010, 0x20004000, 0x20000000, 0x00004010,\n\t0x00000000, 0x00400010, 0x20004010, 0x00004000,\n\t0x00404000, 0x20004010, 0x00000010, 0x20400010,\n\t0x20400010, 0x00000000, 0x00404010, 0x20404000,\n\t0x00004010, 0x00404000, 0x20404000, 0x20000000,\n\t0x20004000, 0x00000010, 0x20400010, 0x00404000,\n\t0x20404010, 0x00400000, 0x00004010, 0x20000010,\n\t0x00400000, 0x20004000, 0x20000000, 0x00004010,\n\t0x20000010, 0x20404010, 0x00404000, 0x20400000,\n\t0x00404010, 0x20404000, 0x00000000, 0x20400010,\n\t0x00000010, 0x00004000, 0x20400000, 0x00404010,\n\t0x00004000, 0x00400010, 0x20004010, 0x00000000,\n\t0x20404000, 0x20000000, 0x00400010, 0x20004010\n};\n\nstatic uint32_t SB7[64] = {\n\t0x00200000, 0x04200002, 0x04000802, 0x00000000,\n\t0x00000800, 0x04000802, 0x00200802, 0x04200800,\n\t0x04200802, 0x00200000, 0x00000000, 0x04000002,\n\t0x00000002, 0x04000000, 0x04200002, 0x00000802,\n\t0x04000800, 0x00200802, 0x00200002, 0x04000800,\n\t0x04000002, 0x04200000, 0x04200800, 0x00200002,\n\t0x04200000, 0x00000800, 0x00000802, 0x04200802,\n\t0x00200800, 0x00000002, 0x04000000, 0x00200800,\n\t0x04000000, 0x00200800, 0x00200000, 0x04000802,\n\t0x04000802, 0x04200002, 0x04200002, 0x00000002,\n\t0x00200002, 0x04000000, 0x04000800, 0x00200000,\n\t0x04200800, 0x00000802, 0x00200802, 0x04200800,\n\t0x00000802, 0x04000002, 0x04200802, 0x04200000,\n\t0x00200800, 0x00000000, 0x00000002, 0x04200802,\n\t0x00000000, 0x00200802, 0x04200000, 0x00000800,\n\t0x04000002, 0x04000800, 0x00000800, 0x00200002\n};\n\nstatic uint32_t SB8[64] = {\n\t0x10001040, 0x00001000, 0x00040000, 0x10041040,\n\t0x10000000, 0x10001040, 0x00000040, 0x10000000,\n\t0x00040040, 0x10040000, 0x10041040, 0x00041000,\n\t0x10041000, 0x00041040, 0x00001000, 0x00000040,\n\t0x10040000, 0x10000040, 0x10001000, 0x00001040,\n\t0x00041000, 0x00040040, 0x10040040, 0x10041000,\n\t0x00001040, 0x00000000, 0x00000000, 0x10040040,\n\t0x10000040, 0x10001000, 0x00041040, 0x00040000,\n\t0x00041040, 0x00040000, 0x10041000, 0x00001000,\n\t0x00000040, 0x10040040, 0x00001000, 0x00041040,\n\t0x10001000, 0x00000040, 0x10000040, 0x10040000,\n\t0x10040040, 0x10000000, 0x00040000, 0x10001040,\n\t0x00000000, 0x10041040, 0x00040040, 0x10000040,\n\t0x10040000, 0x10001000, 0x10001040, 0x00000000,\n\t0x10041040, 0x00041000, 0x00041000, 0x00001040,\n\t0x00001040, 0x00040040, 0x10000000, 0x10041000\n};\n\n/* PC1: left and right halves bit-swap */\n\nstatic uint32_t LHs[16] = {\n\t0x00000000, 0x00000001, 0x00000100, 0x00000101,\n\t0x00010000, 0x00010001, 0x00010100, 0x00010101,\n\t0x01000000, 0x01000001, 0x01000100, 0x01000101,\n\t0x01010000, 0x01010001, 0x01010100, 0x01010101\n};\n\nstatic uint32_t RHs[16] = {\n\t0x00000000, 0x01000000, 0x00010000, 0x01010000,\n\t0x00000100, 0x01000100, 0x00010100, 0x01010100,\n\t0x00000001, 0x01000001, 0x00010001, 0x01010001,\n\t0x00000101, 0x01000101, 0x00010101, 0x01010101,\n};\n\n/* platform-independant 32-bit integer manipulation macros */\n\n#define GET_UINT32(n,b,i)\t\t\t\t\t   \\\n{\t\t\t\t\t\t\t\t\t\t\t   \\\n\t(n) = ( (uint32_t) (b)[(i)\t] << 24 )\t   \\\n\t\t| ( (uint32_t) (b)[(i) + 1] << 16 )\t   \\\n\t\t| ( (uint32_t) (b)[(i) + 2] <<  8 )\t   \\\n\t\t| ( (uint32_t) (b)[(i) + 3]\t   );\t  \\\n}\n\n#define PUT_UINT32(n,b,i)\t\t\t\t\t   \\\n{\t\t\t\t\t\t\t\t\t\t\t   \\\n\t(b)[(i)\t] = (uint8_t) ( (n) >> 24 );\t   \\\n\t(b)[(i) + 1] = (uint8_t) ( (n) >> 16 );\t   \\\n\t(b)[(i) + 2] = (uint8_t) ( (n) >>  8 );\t   \\\n\t(b)[(i) + 3] = (uint8_t) ( (n)\t   );\t   \\\n}\n\n/* Initial Permutation macro */\n\n#define DES_IP(X,Y)\t\t\t\t\t\t\t\t\t\t\t \\\n{\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t   \\\n\tT = ((X >>  4) ^ Y) & 0x0F0F0F0F; Y ^= T; X ^= (T <<  4);   \\\n\tT = ((X >> 16) ^ Y) & 0x0000FFFF; Y ^= T; X ^= (T << 16);   \\\n\tT = ((Y >>  2) ^ X) & 0x33333333; X ^= T; Y ^= (T <<  2);   \\\n\tT = ((Y >>  8) ^ X) & 0x00FF00FF; X ^= T; Y ^= (T <<  8);   \\\n\tY = ((Y << 1) | (Y >> 31)) & 0xFFFFFFFF;\t\t\t\t\t\\\n\tT = (X ^ Y) & 0xAAAAAAAA; Y ^= T; X ^= T;\t\t\t\t   \\\n\tX = ((X << 1) | (X >> 31)) & 0xFFFFFFFF;\t\t\t\t\t\\\n}\n\n/* Final Permutation macro */\n\n#define DES_FP(X,Y)\t\t\t\t\t\t\t\t\t\t\t \\\n{\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t   \\\n\tX = ((X << 31) | (X >> 1)) & 0xFFFFFFFF;\t\t\t\t\t\\\n\tT = (X ^ Y) & 0xAAAAAAAA; X ^= T; Y ^= T;\t\t\t\t   \\\n\tY = ((Y << 31) | (Y >> 1)) & 0xFFFFFFFF;\t\t\t\t\t\\\n\tT = ((Y >>  8) ^ X) & 0x00FF00FF; X ^= T; Y ^= (T <<  8);   \\\n\tT = ((Y >>  2) ^ X) & 0x33333333; X ^= T; Y ^= (T <<  2);   \\\n\tT = ((X >> 16) ^ Y) & 0x0000FFFF; Y ^= T; X ^= (T << 16);   \\\n\tT = ((X >>  4) ^ Y) & 0x0F0F0F0F; Y ^= T; X ^= (T <<  4);   \\\n}\n\n/* DES round macro */\n\n#define DES_ROUND(X,Y)\t\t\t\t\t\t  \\\n{\t\t\t\t\t\t\t\t\t\t\t   \\\n\tT = *SK++ ^ X;\t\t\t\t\t\t\t  \\\n\tY ^= SB8[ (T\t  ) & 0x3F ] ^\t\t\t  \\\n\t\t SB6[ (T >>  8) & 0x3F ] ^\t\t\t  \\\n\t\t SB4[ (T >> 16) & 0x3F ] ^\t\t\t  \\\n\t\t SB2[ (T >> 24) & 0x3F ];\t\t\t   \\\n\t\t\t\t\t\t\t\t\t\t\t\t\\\n\tT = *SK++ ^ ((X << 28) | (X >> 4));\t\t \\\n\tY ^= SB7[ (T\t  ) & 0x3F ] ^\t\t\t  \\\n\t\t SB5[ (T >>  8) & 0x3F ] ^\t\t\t  \\\n\t\t SB3[ (T >> 16) & 0x3F ] ^\t\t\t  \\\n\t\t SB1[ (T >> 24) & 0x3F ];\t\t\t   \\\n}\n\n/* DES key schedule */\n\nstatic void \ndes_main_ks( uint32_t SK[32], const uint8_t key[8] ) {\n\tint i;\n\tuint32_t X, Y, T;\n\n\tGET_UINT32( X, key, 0 );\n\tGET_UINT32( Y, key, 4 );\n\n\t/* Permuted Choice 1 */\n\n\tT =  ((Y >>  4) ^ X) & 0x0F0F0F0F;  X ^= T; Y ^= (T <<  4);\n\tT =  ((Y\t  ) ^ X) & 0x10101010;  X ^= T; Y ^= (T\t  );\n\n\tX =   (LHs[ (X\t  ) & 0xF] << 3) | (LHs[ (X >>  8) & 0xF ] << 2)\n\t\t| (LHs[ (X >> 16) & 0xF] << 1) | (LHs[ (X >> 24) & 0xF ]\t )\n\t\t| (LHs[ (X >>  5) & 0xF] << 7) | (LHs[ (X >> 13) & 0xF ] << 6)\n\t\t| (LHs[ (X >> 21) & 0xF] << 5) | (LHs[ (X >> 29) & 0xF ] << 4);\n\n\tY =   (RHs[ (Y >>  1) & 0xF] << 3) | (RHs[ (Y >>  9) & 0xF ] << 2)\n\t\t| (RHs[ (Y >> 17) & 0xF] << 1) | (RHs[ (Y >> 25) & 0xF ]\t )\n\t\t| (RHs[ (Y >>  4) & 0xF] << 7) | (RHs[ (Y >> 12) & 0xF ] << 6)\n\t\t| (RHs[ (Y >> 20) & 0xF] << 5) | (RHs[ (Y >> 28) & 0xF ] << 4);\n\n\tX &= 0x0FFFFFFF;\n\tY &= 0x0FFFFFFF;\n\n\t/* calculate subkeys */\n\n\tfor( i = 0; i < 16; i++ )\n\t{\n\t\tif( i < 2 || i == 8 || i == 15 )\n\t\t{\n\t\t\tX = ((X <<  1) | (X >> 27)) & 0x0FFFFFFF;\n\t\t\tY = ((Y <<  1) | (Y >> 27)) & 0x0FFFFFFF;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tX = ((X <<  2) | (X >> 26)) & 0x0FFFFFFF;\n\t\t\tY = ((Y <<  2) | (Y >> 26)) & 0x0FFFFFFF;\n\t\t}\n\n\t\t*SK++ =   ((X <<  4) & 0x24000000) | ((X << 28) & 0x10000000)\n\t\t\t\t| ((X << 14) & 0x08000000) | ((X << 18) & 0x02080000)\n\t\t\t\t| ((X <<  6) & 0x01000000) | ((X <<  9) & 0x00200000)\n\t\t\t\t| ((X >>  1) & 0x00100000) | ((X << 10) & 0x00040000)\n\t\t\t\t| ((X <<  2) & 0x00020000) | ((X >> 10) & 0x00010000)\n\t\t\t\t| ((Y >> 13) & 0x00002000) | ((Y >>  4) & 0x00001000)\n\t\t\t\t| ((Y <<  6) & 0x00000800) | ((Y >>  1) & 0x00000400)\n\t\t\t\t| ((Y >> 14) & 0x00000200) | ((Y\t  ) & 0x00000100)\n\t\t\t\t| ((Y >>  5) & 0x00000020) | ((Y >> 10) & 0x00000010)\n\t\t\t\t| ((Y >>  3) & 0x00000008) | ((Y >> 18) & 0x00000004)\n\t\t\t\t| ((Y >> 26) & 0x00000002) | ((Y >> 24) & 0x00000001);\n\n\t\t*SK++ =   ((X << 15) & 0x20000000) | ((X << 17) & 0x10000000)\n\t\t\t\t| ((X << 10) & 0x08000000) | ((X << 22) & 0x04000000)\n\t\t\t\t| ((X >>  2) & 0x02000000) | ((X <<  1) & 0x01000000)\n\t\t\t\t| ((X << 16) & 0x00200000) | ((X << 11) & 0x00100000)\n\t\t\t\t| ((X <<  3) & 0x00080000) | ((X >>  6) & 0x00040000)\n\t\t\t\t| ((X << 15) & 0x00020000) | ((X >>  4) & 0x00010000)\n\t\t\t\t| ((Y >>  2) & 0x00002000) | ((Y <<  8) & 0x00001000)\n\t\t\t\t| ((Y >> 14) & 0x00000808) | ((Y >>  9) & 0x00000400)\n\t\t\t\t| ((Y\t  ) & 0x00000200) | ((Y <<  7) & 0x00000100)\n\t\t\t\t| ((Y >>  7) & 0x00000020) | ((Y >>  3) & 0x00000011)\n\t\t\t\t| ((Y <<  2) & 0x00000004) | ((Y >> 21) & 0x00000002);\n\t}\n}\n\n/* DES 64-bit block encryption/decryption */\n\nstatic void \ndes_crypt( const uint32_t SK[32], const uint8_t input[8], uint8_t output[8] ) {\n\tuint32_t X, Y, T;\n\n\tGET_UINT32( X, input, 0 );\n\tGET_UINT32( Y, input, 4 );\n\n\tDES_IP( X, Y );\n\n\tDES_ROUND( Y, X );  DES_ROUND( X, Y );\n\tDES_ROUND( Y, X );  DES_ROUND( X, Y );\n\tDES_ROUND( Y, X );  DES_ROUND( X, Y );\n\tDES_ROUND( Y, X );  DES_ROUND( X, Y );\n\tDES_ROUND( Y, X );  DES_ROUND( X, Y );\n\tDES_ROUND( Y, X );  DES_ROUND( X, Y );\n\tDES_ROUND( Y, X );  DES_ROUND( X, Y );\n\tDES_ROUND( Y, X );  DES_ROUND( X, Y );\n\n\tDES_FP( Y, X );\n\n\tPUT_UINT32( Y, output, 0 );\n\tPUT_UINT32( X, output, 4 );\n}\n\nstatic int\nlrandomkey(lua_State *L) {\n\tchar tmp[8];\n\tint i;\n\tchar x = 0;\n\tfor (i=0;i<8;i++) {\n\t\ttmp[i] = rand() & 0xff;\n\t\tx ^= tmp[i];\n\t}\n\tif (x==0) {\n\t\ttmp[0] |= 1;\t// avoid 0\n\t}\n\tlua_pushlstring(L, tmp, 8);\n\treturn 1;\n}\n\nstatic void\npadding_mode_table(lua_State *L) {\n\t// see macros PADDING_MODE_ISO7816_4, etc.\n\tconst char * mode[] = {\n\t\t\"iso7816_4\",\n\t\t\"pkcs7\",\n\t};\n\tint n = sizeof(mode) / sizeof(mode[0]);\n\tint i;\n\tlua_createtable(L,0,n);\n\tfor (i=0;i<n;i++) {\n\t\tlua_pushinteger(L, i);\n\t\tlua_setfield(L, -2, mode[i]);\n\t}\n}\n\ntypedef void (*padding_add)(uint8_t buf[8], int offset);\ntypedef int (*padding_remove)(const uint8_t *last);\n\nstatic void\npadding_add_iso7816_4(uint8_t buf[8], int offset) {\n\tbuf[offset] = 0x80;\n\tmemset(buf+offset+1, 0, 7-offset);\n}\n\nstatic int\npadding_remove_iso7816_4(const uint8_t *last) {\n\tint padding = 1;\n\tint i;\n\tfor (i=0;i<8;i++,last--) {\n\t\tif (*last == 0) {\n\t\t\tpadding++;\n\t\t} else if (*last == 0x80) {\n\t\t\treturn padding;\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n\t// invalid\n\treturn 0;\n}\n\nstatic void\npadding_add_pkcs7(uint8_t buf[8], int offset) {\n\tuint8_t x = 8-offset;\n\tmemset(buf+offset, x, 8-offset);\n}\n\nstatic int\npadding_remove_pkcs7(const uint8_t *last) {\n\tint padding = *last;\n\tint i;\n\tfor (i=1;i<padding;i++) {\n\t\t--last;\n\t\tif (*last != padding)\n\t\t\treturn 0;\t// invalid\n\t}\n\treturn padding;\n}\n\nstatic padding_add padding_add_func[] = {\n\tpadding_add_iso7816_4,\n\tpadding_add_pkcs7,\n};\n\nstatic padding_remove padding_remove_func[] = {\n\tpadding_remove_iso7816_4,\n\tpadding_remove_pkcs7,\n};\n\nstatic inline void\ncheck_padding_mode(lua_State *L, int mode) {\n\tif (mode < 0 || mode >= PADDING_MODE_COUNT)\n\t\tluaL_error(L, \"Invalid padding mode %d\", mode);\n}\n\nstatic void\nadd_padding(lua_State *L, uint8_t buf[8], const uint8_t *src, int offset, int mode) {\n\tcheck_padding_mode(L, mode);\n\tif (offset >= 8)\n\t\tluaL_error(L, \"Invalid padding\");\n\tmemcpy(buf, src, offset);\n\tpadding_add_func[mode](buf, offset);\n}\n\nstatic int\nremove_padding(lua_State *L, const uint8_t *last, int mode) {\n\tcheck_padding_mode(L, mode);\n\treturn padding_remove_func[mode](last);\n}\n\nstatic void\ndes_key(lua_State *L, uint32_t SK[32]) {\n\tsize_t keysz = 0;\n\tconst void * key = luaL_checklstring(L, 1, &keysz);\n\tif (keysz != 8) {\n\t\tluaL_error(L, \"Invalid key size %d, need 8 bytes\", (int)keysz);\n\t}\n\tdes_main_ks(SK, (const uint8_t*)key);\n}\n\nstatic int\nldesencode(lua_State *L) {\n\tuint32_t SK[32];\n\tdes_key(L, SK);\n\n\tsize_t textsz = 0;\n\tconst uint8_t * text = (const uint8_t *)luaL_checklstring(L, 2, &textsz);\n\tsize_t chunksz = (textsz + 8) & ~7;\n\tint padding_mode = luaL_optinteger(L, 3, PADDING_MODE_ISO7816_4);\n\tuint8_t tmp[SMALL_CHUNK];\n\tuint8_t *buffer = tmp;\n\tif (chunksz > SMALL_CHUNK) {\n\t\tbuffer = (uint8_t*)lua_newuserdatauv(L, chunksz, 0);\n\t}\n\tint i;\n\tfor (i=0;i<(int)textsz-7;i+=8) {\n\t\tdes_crypt(SK, text+i, buffer+i);\n\t}\n\tuint8_t tail[8];\n\tadd_padding(L, tail, text+i, textsz - i, padding_mode);\n\tdes_crypt(SK, tail, buffer+i);\n\tlua_pushlstring(L, (const char *)buffer, chunksz);\n\n\treturn 1;\n}\n\nstatic int\nldesdecode(lua_State *L) {\n\tuint32_t ESK[32];\n\tdes_key(L, ESK);\n\tuint32_t SK[32];\n\tint i;\n\tfor( i = 0; i < 32; i += 2 ) {\n\t\tSK[i] = ESK[30 - i];\n\t\tSK[i + 1] = ESK[31 - i];\n\t}\n\tsize_t textsz = 0;\n\tconst uint8_t *text = (const uint8_t *)luaL_checklstring(L, 2, &textsz);\n\tif ((textsz & 7) || textsz == 0) {\n\t\treturn luaL_error(L, \"Invalid des crypt text length %d\", (int)textsz);\n\t}\n\tint padding_mode = luaL_optinteger(L, 3, PADDING_MODE_ISO7816_4);\n\tuint8_t tmp[SMALL_CHUNK];\n\tuint8_t *buffer = tmp;\n\tif (textsz > SMALL_CHUNK) {\n\t\tbuffer = (uint8_t*)lua_newuserdatauv(L, textsz, 0);\n\t}\n\tfor (i=0;i<textsz;i+=8) {\n\t\tdes_crypt(SK, text+i, buffer+i);\n\t}\n\tint padding = remove_padding(L, buffer + textsz - 1, padding_mode);\n\tif (padding <= 0 || padding > 8) {\n\t\treturn luaL_error(L, \"Invalid des crypt text\");\n\t}\n\tlua_pushlstring(L, (const char *)buffer, textsz - padding);\n\treturn 1;\n}\n\n\nstatic void\nHash(const char * str, int sz, uint8_t key[8]) {\n\tuint32_t djb_hash = 5381L;\n\tuint32_t js_hash = 1315423911L;\n\n\tint i;\n\tfor (i=0;i<sz;i++) {\n\t\tuint8_t c = (uint8_t)str[i];\n\t\tdjb_hash += (djb_hash << 5) + c;\n\t\tjs_hash ^= ((js_hash << 5) + c + (js_hash >> 2));\n\t}\n\n\tkey[0] = djb_hash & 0xff;\n\tkey[1] = (djb_hash >> 8) & 0xff;\n\tkey[2] = (djb_hash >> 16) & 0xff;\n\tkey[3] = (djb_hash >> 24) & 0xff;\n\n\tkey[4] = js_hash & 0xff;\n\tkey[5] = (js_hash >> 8) & 0xff;\n\tkey[6] = (js_hash >> 16) & 0xff;\n\tkey[7] = (js_hash >> 24) & 0xff;\n}\n\nstatic int\nlhashkey(lua_State *L) {\n\tsize_t sz = 0;\n\tconst char * key = luaL_checklstring(L, 1, &sz);\n\tuint8_t realkey[8];\n\tHash(key,(int)sz,realkey);\n\tlua_pushlstring(L, (const char *)realkey, 8);\n\treturn 1;\n}\n\nstatic int\nltohex(lua_State *L) {\n\tstatic char hex[] = \"0123456789abcdef\";\n\tsize_t sz = 0;\n\tconst uint8_t * text = (const uint8_t *)luaL_checklstring(L, 1, &sz);\n\tchar tmp[SMALL_CHUNK];\n\tchar *buffer = tmp;\n\tif (sz > SMALL_CHUNK/2) {\n\t\tbuffer = (char*)lua_newuserdatauv(L, sz * 2, 0);\n\t}\n\tint i;\n\tfor (i=0;i<sz;i++) {\n\t\tbuffer[i*2] = hex[text[i] >> 4];\n\t\tbuffer[i*2+1] = hex[text[i] & 0xf];\n\t}\n\tlua_pushlstring(L, buffer, sz * 2);\n\treturn 1;\n}\n\n#define HEX(v,c) { char tmp = (char) c; if (tmp >= '0' && tmp <= '9') { v = tmp-'0'; } else { v = tmp - 'a' + 10; } }\n\nstatic int\nlfromhex(lua_State *L) {\n\tsize_t sz = 0;\n\tconst char * text = luaL_checklstring(L, 1, &sz);\n\tif (sz & 1) {\n\t\treturn luaL_error(L, \"Invalid hex text size %d\", (int)sz);\n\t}\n\tchar tmp[SMALL_CHUNK];\n\tchar *buffer = tmp;\n\tif (sz > SMALL_CHUNK*2) {\n\t\tbuffer = (char*)lua_newuserdatauv(L, sz / 2, 0);\n\t}\n\tint i;\n\tfor (i=0;i<sz;i+=2) {\n\t\tuint8_t hi,low;\n\t\tHEX(hi, text[i]);\n\t\tHEX(low, text[i+1]);\n\t\tif (hi > 16 || low > 16) {\n\t\t\treturn luaL_error(L, \"Invalid hex text\", text);\n\t\t}\n\t\tbuffer[i/2] = hi<<4 | low;\n\t}\n\tlua_pushlstring(L, buffer, i/2);\n\treturn 1;\n}\n\n// Constants are the integer part of the sines of integers (in radians) * 2^32.\nstatic const uint32_t k[64] = {\n0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee ,\n0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501 ,\n0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be ,\n0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821 ,\n0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa ,\n0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8 ,\n0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed ,\n0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a ,\n0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c ,\n0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70 ,\n0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05 ,\n0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665 ,\n0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039 ,\n0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1 ,\n0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1 ,\n0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391 };\n \n// r specifies the per-round shift amounts\nstatic const uint32_t r[] = {7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,\n\t\t\t\t\t  5,  9, 14, 20, 5,  9, 14, 20, 5,  9, 14, 20, 5,  9, 14, 20,\n\t\t\t\t\t  4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,\n\t\t\t\t\t  6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21};\n \n// leftrotate function definition\n#define LEFTROTATE(x, c) (((x) << (c)) | ((x) >> (32 - (c))))\n\nstatic void\ndigest_md5(uint32_t w[16], uint32_t result[4]) {\n\tuint32_t a, b, c, d, f, g, temp;\n\tint i;\n \n\ta = 0x67452301u;\n\tb = 0xefcdab89u;\n\tc = 0x98badcfeu;\n\td = 0x10325476u;\n\n\tfor(i = 0; i<64; i++) {\n\t\tif (i < 16) {\n\t\t\tf = (b & c) | ((~b) & d);\n\t\t\tg = i;\n\t\t} else if (i < 32) {\n\t\t\tf = (d & b) | ((~d) & c);\n\t\t\tg = (5*i + 1) % 16;\n\t\t} else if (i < 48) {\n\t\t\tf = b ^ c ^ d;\n\t\t\tg = (3*i + 5) % 16; \n\t\t} else {\n\t\t\tf = c ^ (b | (~d));\n\t\t\tg = (7*i) % 16;\n\t\t}\n\n\t\ttemp = d;\n\t\td = c;\n\t\tc = b;\n\t\tb = b + LEFTROTATE((a + f + k[i] + w[g]), r[i]);\n\t\ta = temp;\n\t}\n\n\tresult[0] = a;\n\tresult[1] = b;\n\tresult[2] = c;\n\tresult[3] = d;\n}\n\n// hmac64 use md5 algorithm without padding, and the result is (c^d .. a^b)\nstatic void\nhmac(uint32_t x[2], uint32_t y[2], uint32_t result[2]) {\n\tuint32_t w[16];\n\tuint32_t r[4];\n\tint i;\n\tfor (i=0;i<16;i+=4) {\n\t\tw[i] = x[1];\n\t\tw[i+1] = x[0];\n\t\tw[i+2] = y[1];\n\t\tw[i+3] = y[0];\n\t}\n\n\tdigest_md5(w,r);\n\n\tresult[0] = r[2]^r[3];\n\tresult[1] = r[0]^r[1];\n}\n\nstatic void\nhmac_md5(uint32_t x[2], uint32_t y[2], uint32_t result[2]) {\n\tuint32_t w[16];\n\tuint32_t r[4];\n\tint i;\n\tfor (i=0;i<12;i+=4) {\n\t\tw[i] = x[0];\n\t\tw[i+1] = x[1];\n\t\tw[i+2] = y[0];\n\t\tw[i+3] = y[1];\n\t}\n\n\tw[12] = 0x80;\n\tw[13] = 0;\n\tw[14] = 384;\n\tw[15] = 0;\n\n\tdigest_md5(w,r);\n\n\tresult[0] = (r[0] + 0x67452301u) ^ (r[2] + 0x98badcfeu);\n\tresult[1] = (r[1] + 0xefcdab89u) ^ (r[3] + 0x10325476u);\n}\n\nstatic void\nread64(lua_State *L, uint32_t xx[2], uint32_t yy[2]) {\n\tsize_t sz = 0;\n\tconst uint8_t *x = (const uint8_t *)luaL_checklstring(L, 1, &sz);\n\tif (sz != 8) {\n\t\tluaL_error(L, \"Invalid uint64 x\");\n\t}\n\tconst uint8_t *y = (const uint8_t *)luaL_checklstring(L, 2, &sz);\n\tif (sz != 8) {\n\t\tluaL_error(L, \"Invalid uint64 y\");\n\t}\n\txx[0] = x[0] | x[1]<<8 | x[2]<<16 | x[3]<<24;\n\txx[1] = x[4] | x[5]<<8 | x[6]<<16 | x[7]<<24;\n\tyy[0] = y[0] | y[1]<<8 | y[2]<<16 | y[3]<<24;\n\tyy[1] = y[4] | y[5]<<8 | y[6]<<16 | y[7]<<24;\n}\n\nstatic int\npushqword(lua_State *L, uint32_t result[2]) {\n\tuint8_t tmp[8];\n\ttmp[0] = result[0] & 0xff;\n\ttmp[1] = (result[0] >> 8 )& 0xff;\n\ttmp[2] = (result[0] >> 16 )& 0xff;\n\ttmp[3] = (result[0] >> 24 )& 0xff;\n\ttmp[4] = result[1] & 0xff;\n\ttmp[5] = (result[1] >> 8 )& 0xff;\n\ttmp[6] = (result[1] >> 16 )& 0xff;\n\ttmp[7] = (result[1] >> 24 )& 0xff;\n\n\tlua_pushlstring(L, (const char *)tmp, 8);\n\treturn 1;\n}\n\nstatic int\nlhmac64(lua_State *L) {\n\tuint32_t x[2], y[2];\n\tread64(L, x, y);\n\tuint32_t result[2];\n\thmac(x,y,result);\n\treturn pushqword(L, result);\n}\n\n/*\n  h1 = crypt.hmac64_md5(a,b)\n  m = md5.sum((a..b):rep(3))\n  h2 = crypt.xor_str(m:sub(1,8), m:sub(9,16))\n  assert(h1 == h2)\n */\nstatic int\nlhmac64_md5(lua_State *L) {\n\tuint32_t x[2], y[2];\n\tread64(L, x, y);\n\tuint32_t result[2];\n\thmac_md5(x,y,result);\n\treturn pushqword(L, result);\n}\n\n/*\n\t8bytes key\n\tstring text\n */\nstatic int\nlhmac_hash(lua_State *L) {\n\tuint32_t key[2];\n\tsize_t sz = 0;\n\tconst uint8_t *x = (const uint8_t *)luaL_checklstring(L, 1, &sz);\n\tif (sz != 8) {\n\t\tluaL_error(L, \"Invalid uint64 key\");\n\t}\n\tkey[0] = x[0] | x[1]<<8 | x[2]<<16 | x[3]<<24;\n\tkey[1] = x[4] | x[5]<<8 | x[6]<<16 | x[7]<<24;\n\tconst char * text = luaL_checklstring(L, 2, &sz);\n\tuint8_t h[8];\n\tHash(text,(int)sz,h);\n\tuint32_t htext[2];\n\thtext[0] = h[0] | h[1]<<8 | h[2]<<16 | h[3]<<24;\n\thtext[1] = h[4] | h[5]<<8 | h[6]<<16 | h[7]<<24;\n\tuint32_t result[2];\n\thmac(htext,key,result);\n\treturn pushqword(L, result);\n}\n\n// powmodp64 for DH-key exchange\n\n// The biggest 64bit prime\n#define P 0xffffffffffffffc5ull\n\nstatic inline uint64_t\nmul_mod_p(uint64_t a, uint64_t b) {\n\tuint64_t m = 0;\n\twhile(b) {\n\t\tif(b&1) {\n\t\t\tuint64_t t = P-a;\n\t\t\tif ( m >= t) {\n\t\t\t\tm -= t;\n\t\t\t} else {\n\t\t\t\tm += a;\n\t\t\t}\n\t\t}\n\t\tif (a >= P - a) {\n\t\t\ta = a * 2 - P;\n\t\t} else {\n\t\t\ta = a * 2;\n\t\t}\n\t\tb>>=1;\n\t}\n\treturn m;\n}\n\nstatic inline uint64_t\npow_mod_p(uint64_t a, uint64_t b) {\n\tif (b==1) {\n\t\treturn a;\n\t}\n\tuint64_t t = pow_mod_p(a, b>>1);\n\tt = mul_mod_p(t,t);\n\tif (b % 2) {\n\t\tt = mul_mod_p(t, a);\n\t}\n\treturn t;\n}\n\n// calc a^b % p\nstatic uint64_t\npowmodp(uint64_t a, uint64_t b) {\n\tif (a > P)\n\t\ta%=P;\n\treturn pow_mod_p(a,b);\n}\n\nstatic void\npush64(lua_State *L, uint64_t r) {\n\tuint8_t tmp[8];\n\ttmp[0] = r & 0xff;\n\ttmp[1] = (r >> 8 )& 0xff;\n\ttmp[2] = (r >> 16 )& 0xff;\n\ttmp[3] = (r >> 24 )& 0xff;\n\ttmp[4] = (r >> 32 )& 0xff;\n\ttmp[5] = (r >> 40 )& 0xff;\n\ttmp[6] = (r >> 48 )& 0xff;\n\ttmp[7] = (r >> 56 )& 0xff;\n\n\tlua_pushlstring(L, (const char *)tmp, 8);\n}\n\nstatic int\nldhsecret(lua_State *L) {\n\tuint32_t x[2], y[2];\n\tread64(L, x, y);\n\tuint64_t xx = (uint64_t)x[0] | (uint64_t)x[1]<<32;\n\tuint64_t yy = (uint64_t)y[0] | (uint64_t)y[1]<<32;\n\tif (xx == 0 || yy == 0)\n\t\treturn luaL_error(L, \"Can't be 0\");\n\tuint64_t r = powmodp(xx, yy);\n\n\tpush64(L, r);\n\n\treturn 1;\n}\n\n#define G 5\n\nstatic int\nldhexchange(lua_State *L) {\n\tsize_t sz = 0;\n\tconst uint8_t *x = (const uint8_t *)luaL_checklstring(L, 1, &sz);\n\tif (sz != 8) {\n\t\tluaL_error(L, \"Invalid dh uint64 key\");\n\t}\n\tuint32_t xx[2];\n\txx[0] = x[0] | x[1]<<8 | x[2]<<16 | x[3]<<24;\n\txx[1] = x[4] | x[5]<<8 | x[6]<<16 | x[7]<<24;\n\n\tuint64_t x64 = (uint64_t)xx[0] | (uint64_t)xx[1]<<32;\n\tif (x64 == 0)\n\t\treturn luaL_error(L, \"Can't be 0\");\n\n\tuint64_t r = powmodp(G,\tx64);\n\tpush64(L, r);\n\treturn 1;\n}\n\n// base64\n\nstatic int\nlb64encode(lua_State *L) {\n\tstatic const char* encoding = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n\tsize_t sz = 0;\n\tconst uint8_t * text = (const uint8_t *)luaL_checklstring(L, 1, &sz);\n\tint encode_sz = (sz + 2)/3*4;\n\tchar tmp[SMALL_CHUNK];\n\tchar *buffer = tmp;\n\tif (encode_sz > SMALL_CHUNK) {\n\t\tbuffer = (char*)lua_newuserdatauv(L, encode_sz, 0);\n\t}\n\tint i,j;\n\tj=0;\n\tfor (i=0;i<(int)sz-2;i+=3) {\n\t\tuint32_t v = text[i] << 16 | text[i+1] << 8 | text[i+2];\n\t\tbuffer[j] = encoding[v >> 18];\n\t\tbuffer[j+1] = encoding[(v >> 12) & 0x3f];\n\t\tbuffer[j+2] = encoding[(v >> 6) & 0x3f];\n\t\tbuffer[j+3] = encoding[(v) & 0x3f];\n\t\tj+=4;\n\t}\n\tint padding = sz-i;\n\tuint32_t v;\n\tswitch(padding) {\n\tcase 1 :\n\t\tv = text[i];\n\t\tbuffer[j] = encoding[v >> 2];\n\t\tbuffer[j+1] = encoding[(v & 3) << 4];\n\t\tbuffer[j+2] = '=';\n\t\tbuffer[j+3] = '=';\n\t\tbreak;\n\tcase 2 :\n\t\tv = text[i] << 8 | text[i+1];\n\t\tbuffer[j] = encoding[v >> 10];\n\t\tbuffer[j+1] = encoding[(v >> 4) & 0x3f];\n\t\tbuffer[j+2] = encoding[(v & 0xf) << 2];\n\t\tbuffer[j+3] = '=';\n\t\tbreak;\n\t}\n\tlua_pushlstring(L, buffer, encode_sz);\n\treturn 1;\n}\n\nstatic inline int\nb64index(uint8_t c) {\n\tstatic const int decoding[] = {62,-1,-1,-1,63,52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-2,-1,-1,-1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,-1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51};\n\tint decoding_size = sizeof(decoding)/sizeof(decoding[0]);\n\tif (c<43) {\n\t\treturn -1;\n\t}\n\tc -= 43;\n\tif (c>=decoding_size)\n\t\treturn -1;\n\treturn decoding[c];\n}\n\nstatic int\nlb64decode(lua_State *L) {\n\tsize_t sz = 0;\n\tconst uint8_t * text = (const uint8_t *)luaL_checklstring(L, 1, &sz);\n\tint decode_sz = (sz+3)/4*3;\n\tchar tmp[SMALL_CHUNK];\n\tchar *buffer = tmp;\n\tif (decode_sz > SMALL_CHUNK) {\n\t\tbuffer = (char*)lua_newuserdatauv(L, decode_sz, 0);\n\t}\n\tint i,j;\n\tint output = 0;\n\tfor (i=0;i<sz;) {\n\t\tint padding = 0;\n\t\tint c[4];\n\t\tfor (j=0;j<4;) {\n\t\t\tif (i>=sz && 4>j){\n\t\t\t\t/*To improve compatibility, there may not be enough equal signs */ \n\t\t\t\tc[j] = -2;   \n\t\t\t}else{\n\t\t\t\tc[j] = b64index(text[i]);\n\t\t\t}\n\t\t\tif (c[j] == -1) {\n\t\t\t\t++i;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (c[j] == -2) {\n\t\t\t\t++padding;\n\t\t\t}\n\t\t\t++i;\n\t\t\t++j;\n\t\t}\n\t\tuint32_t v;\n\t\tswitch (padding) {\n\t\tcase 0:\n\t\t\tv = (unsigned)c[0] << 18 | c[1] << 12 | c[2] << 6 | c[3];\n\t\t\tbuffer[output] = v >> 16;\n\t\t\tbuffer[output+1] = (v >> 8) & 0xff;\n\t\t\tbuffer[output+2] = v & 0xff;\n\t\t\toutput += 3;\n\t\t\tbreak;\n\t\tcase 1:\n\t\t\tif (c[3] != -2 || (c[2] & 3)!=0) {\n\t\t\t\treturn luaL_error(L, \"Invalid base64 text\");\n\t\t\t}\n\t\t\tv = (unsigned)c[0] << 10 | c[1] << 4 | c[2] >> 2 ;\n\t\t\tbuffer[output] = v >> 8;\n\t\t\tbuffer[output+1] = v & 0xff;\n\t\t\toutput += 2;\n\t\t\tbreak;\n\t\tcase 2:\n\t\t\tif (c[3] != -2 || c[2] != -2 || (c[1] & 0xf) !=0)  {\n\t\t\t\treturn luaL_error(L, \"Invalid base64 text\");\n\t\t\t}\n\t\t\tv = (unsigned)c[0] << 2 | c[1] >> 4;\n\t\t\tbuffer[output] = v;\n\t\t\t++ output;\n\t\t\tbreak;\n\t\tdefault:\n\t\t\treturn luaL_error(L, \"Invalid base64 text\");\n\t\t}\n\t}\n\tlua_pushlstring(L, buffer, output);\n\treturn 1;\n}\n\nstatic int\nlxor_str(lua_State *L) {\n\tsize_t len1,len2;\n\tconst char *s1 = luaL_checklstring(L,1,&len1);\n\tconst char *s2 = luaL_checklstring(L,2,&len2);\n\tif (len2 == 0) {\n\t\treturn luaL_error(L, \"Can't xor empty string\");\n\t}\n\tluaL_Buffer b;\n\tchar * buffer = luaL_buffinitsize(L, &b, len1);\n\tint i;\n\tfor (i=0;i<len1;i++) {\n\t\tbuffer[i] = s1[i] ^ s2[i % len2];\n\t}\n\tluaL_addsize(&b, len1);\n\tluaL_pushresult(&b);\n\treturn 1;\n}\n\n// defined in lsha1.c\nint lsha1(lua_State *L);\nint lhmac_sha1(lua_State *L);\n\n\nint\nluaopen_skynet_crypt(lua_State *L) {\n\tluaL_checkversion(L);\n\tstatic int init = 0;\n\tif (!init) {\n\t\t// Don't need call srandom more than once.\n\t\tinit = 1 ;\n\t\tsrand((rand() << 8) ^ (time(NULL) << 16));\n\t}\n\tluaL_Reg l[] = {\n\t\t{ \"hashkey\", lhashkey },\n\t\t{ \"randomkey\", lrandomkey },\n\t\t{ \"desencode\", ldesencode },\n\t\t{ \"desdecode\", ldesdecode },\n\t\t{ \"hexencode\", ltohex },\n\t\t{ \"hexdecode\", lfromhex },\n\t\t{ \"hmac64\", lhmac64 },\n\t\t{ \"hmac64_md5\", lhmac64_md5 },\n\t\t{ \"dhexchange\", ldhexchange },\n\t\t{ \"dhsecret\", ldhsecret },\n\t\t{ \"base64encode\", lb64encode },\n\t\t{ \"base64decode\", lb64decode },\n\t\t{ \"sha1\", lsha1 },\n\t\t{ \"hmac_sha1\", lhmac_sha1 },\n\t\t{ \"hmac_hash\", lhmac_hash },\n\t\t{ \"xor_str\", lxor_str },\n\t\t{ \"padding\", NULL },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L,l);\n\n\tpadding_mode_table(L);\n\tlua_setfield(L, -2, \"padding\");\n\n\treturn 1;\n}\n"
  },
  {
    "path": "src/lfs.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n#include <assert.h>\n\n#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)\n\n#define LONGPATH_MAX 4096\n\n#include <windows.h>\n#include <Shlobj.h>\n#include <sys/stat.h>\n#include <wchar.h>\n\n#define STAT_STRUCT struct _stati64\n#define STAT_FUNC _wstati64\n\n#ifndef S_ISDIR\n#define S_ISDIR(mode)  (mode&_S_IFDIR)\n#endif\n#ifndef S_ISREG\n#define S_ISREG(mode)  (mode&_S_IFREG)\n#endif\n#ifndef S_ISLNK\n#define S_ISLNK(mode)  (0)\n#endif\n#ifndef S_ISSOCK\n#define S_ISSOCK(mode)  (0)\n#endif\n#ifndef S_ISFIFO\n#define S_ISFIFO(mode)  (0)\n#endif\n#ifndef S_ISCHR\n#define S_ISCHR(mode)  (mode&_S_IFCHR)\n#endif\n#ifndef S_ISBLK\n#define S_ISBLK(mode)  (0)\n#endif\n\nstatic int\nutf8_filename(lua_State *L, const wchar_t * winfilename, int wsz, char *utf8buffer, int sz) {\n\tint result = WideCharToMultiByte(CP_UTF8, 0, winfilename, wsz, utf8buffer, sz, NULL, NULL);\n\tif (result == 0)\n\t\treturn luaL_error(L, \"convert to utf-8 filename fail\");\n\tif (wsz < 0)\t// not include end \\0\n\t\treturn result - 1;\n\tif (result >= sz)\n\t\treturn luaL_error(L, \"convert to utf-8 filename : buffer overflow\");\n\tutf8buffer[result] = 0;\n\treturn result;\n}\n\n#define DIR_METATABLE \"SOLUNA_DIR\"\n\nstruct dir_data {\n\tHANDLE findfile;\n\tint closed;\n};\n\nstatic int\nwindows_filename(lua_State *L, const char * utf8filename, int usz, wchar_t * winbuffer, int wsz) {\n\tint result = MultiByteToWideChar(CP_UTF8, 0, utf8filename, usz, winbuffer, wsz);\n\tif (result == 0)\n\t\treturn luaL_error(L, \"convert to windows utf-16 filename fail\");\n\tif (result < 0)\n\t\treturn result - 1;\n\tif (result >= wsz)\n\t\treturn luaL_error(L, \"convert to windows utf-16 filename : buffer overflow\");\n\twinbuffer[result] = 0;\n\treturn result;\n}\n\nstatic void\nsystem_error(lua_State *L, DWORD errcode) {\n\twchar_t * errormsg;\n\tDWORD n = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,\n\t\tNULL,\n\t\terrcode, 0,\n\t\t(void *)&errormsg, sizeof(errormsg),\n\t\tNULL);\n\tif (n == 0) {\n\t\tlua_pushfstring(L, \"Unknown error %04X\", errcode);\n\t} else {\n\t\tint i;\n\t\tfor (i=n;i>=0;i--) {\n\t\t\tif (errormsg[i] == 0 || errormsg[i] == '\\n' || errormsg[i] == '\\r')\n\t\t\t\t--n;\n\t\t\telse {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tchar tmp[LONGPATH_MAX];\n\t\tint len = utf8_filename(L, errormsg, n, tmp, LONGPATH_MAX);\n\t\tlua_pushlstring(L, tmp, len);\n\t\tHeapFree(GetProcessHeap(), 0, errormsg);\n\t}\n}\n\nstatic int\nerror_return(lua_State *L) {\n\tlua_pushnil(L);\n\tsystem_error(L, GetLastError());\n\treturn 2;\n}\n\nstatic void\npush_filename(lua_State *L, WIN32_FIND_DATAW *data) {\n\tchar firstname[LONGPATH_MAX];\n\tint ulen = utf8_filename(L, data->cFileName, -1, firstname, LONGPATH_MAX);\n\tlua_pushlstring(L, firstname, ulen);\n}\n\nstatic int\ndir_iter(lua_State *L) {\n\tstruct dir_data *d = luaL_checkudata(L, 1, DIR_METATABLE);\n\tluaL_argcheck (L, d->closed == 0, 1, \"closed directory\");\n\tif (d->findfile == INVALID_HANDLE_VALUE) {\n\t\t// no find found\n\t\td->closed = 1;\n\t\treturn 0;\n\t}\n\tif (lua_getuservalue(L, 1) == LUA_TSTRING) {\n\t\t// find time\n\t\tlua_pushnil(L);\n\t\tlua_setuservalue(L, 1);\n\t\treturn 1;\n\t} else {\n\t\tWIN32_FIND_DATAW data;\n\t\tif (FindNextFileW(d->findfile, &data)) {\n\t\t\tpush_filename(L, &data);\n\t\t\treturn 1;\n\t\t} else {\n\t\t\tDWORD errcode = GetLastError();\n\t\t\tFindClose(d->findfile);\n\t\t\td->findfile = INVALID_HANDLE_VALUE;\n\t\t\td->closed = 1;\n\t\t\tif (errcode == ERROR_NO_MORE_FILES)\n\t\t\t\treturn 0;\n\t\t\tlua_pushnil(L);\n\t\t\tsystem_error(L, errcode);\n\t\t\treturn 2;\n\t\t}\n\t}\n}\n\nstatic int\ndir_close(lua_State *L) {\n\tstruct dir_data *d = luaL_checkudata(L, 1, DIR_METATABLE);\n\tif (d->findfile != INVALID_HANDLE_VALUE) {\n\t\tFindClose(d->findfile);\n\t\td->findfile = INVALID_HANDLE_VALUE;\n\t}\n\td->closed = 1;\n\treturn 0;\n}\n\nstatic int\nldir(lua_State *L) {\n\tsize_t sz;\n\tconst char * pathname = luaL_checklstring(L, 1, &sz);\n\twchar_t winname[LONGPATH_MAX-3];\n\tint winsz = windows_filename(L, pathname, sz, winname, LONGPATH_MAX-3);\n\twinname[winsz] = '\\\\';\n\twinname[winsz+1] = '*';\n\twinname[winsz+2] = 0;\n\tWIN32_FIND_DATAW data;\n\tHANDLE findfile = FindFirstFileW(winname, &data);\n\tlua_pushcfunction(L, dir_iter);\n\tif (findfile == INVALID_HANDLE_VALUE) {\n\t\tDWORD errcode = GetLastError();\n\t\tif (errcode == ERROR_FILE_NOT_FOUND) {\n\t\t\tstruct dir_data *d = lua_newuserdata(L, sizeof(*d));\n\t\t\td->findfile = INVALID_HANDLE_VALUE;\n\t\t\td->closed = 0;\n\t\t} else {\n\t\t\tsystem_error(L, errcode);\n\t\t\treturn lua_error(L);\n\t\t}\n\t} else {\n\t\tstruct dir_data *d = lua_newuserdata(L, sizeof(*d));\n\t\td->findfile = findfile;\n\t\td->closed = 0;\n\t\tpush_filename(L, &data);\n\t\tlua_setuservalue(L, -2);\t// set firstname\n\t}\n\n\tif (luaL_newmetatable(L, DIR_METATABLE)) {\n\t\tlua_pushvalue(L, -1);\n\t\tlua_setfield(L, -2, \"__index\");\n\n\t\tlua_pushcfunction (L, dir_iter);\n\t\tlua_setfield(L, -2, \"next\");\n\t\tlua_pushcfunction (L, dir_close);\n\t\tlua_setfield(L, -2, \"close\");\n\t\tlua_pushcfunction (L, dir_close);\n\t\tlua_setfield (L, -2, \"__gc\");\n\t}\n\tlua_setmetatable(L, -2);\n\treturn 2;\n}\n\nstatic int\nlpersonaldir(lua_State *L) {\n\twchar_t document[LONGPATH_MAX] = {0};\n\tif (SHGetFolderPathW(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, document) == S_OK) {\n\t\tchar utf8path[LONGPATH_MAX];\n\t\tint sz = utf8_filename(L, document, -1, utf8path, LONGPATH_MAX);\n\t\tlua_pushlstring(L, utf8path, sz);\n\t\treturn 1;\n\t} else {\n\t\treturn error_return(L);\n\t}\n}\n\nstatic int\nlcurrentdir(lua_State *L) {\n\twchar_t path[LONGPATH_MAX];\n\tchar utf8path[LONGPATH_MAX];\n\tDWORD sz = GetCurrentDirectoryW(LONGPATH_MAX, path);\n\tif (sz == 0) {\n\t\treturn error_return(L);\n\t}\n\tint usz = utf8_filename(L, path, -1, utf8path, LONGPATH_MAX);\n\tlua_pushlstring(L, utf8path, usz);\n\treturn 1;\n}\n\nstatic int\nlchdir(lua_State *L) {\n\tsize_t sz;\n\tconst char * utf8path = luaL_checklstring(L, 1, &sz);\n\twchar_t path[LONGPATH_MAX];\n\twindows_filename(L, utf8path, sz, path, LONGPATH_MAX);\n\tif (SetCurrentDirectoryW(path) == 0) {\n\t\treturn error_return(L);\n\t}\n\tlua_pushboolean(L, 1);\n\treturn 1;\n}\n\nstatic const char *\nmode2string (unsigned short mode) {\n\tif ( S_ISREG(mode) )\n\t\treturn \"file\";\n\telse if ( S_ISDIR(mode) )\n\t\treturn \"directory\";\n\telse if ( S_ISLNK(mode) )\n\t\treturn \"link\";\n\telse if ( S_ISSOCK(mode) )\n\t\treturn \"socket\";\n\telse if ( S_ISFIFO(mode) )\n\t\treturn \"named pipe\";\n\telse if ( S_ISCHR(mode) )\n\t\treturn \"char device\";\n\telse if ( S_ISBLK(mode) )\n\t\treturn \"block device\";\n\telse\n\t\treturn \"other\";\n}\n\n/* inode protection mode */\nstatic void push_st_mode (lua_State *L, STAT_STRUCT *info) {\n\tlua_pushstring (L, mode2string (info->st_mode));\n}\n/* device inode resides on */\nstatic void push_st_dev (lua_State *L, STAT_STRUCT *info) {\n\tlua_pushinteger (L, (lua_Integer) info->st_dev);\n}\n/* inode's number */\nstatic void push_st_ino (lua_State *L, STAT_STRUCT *info) {\n\tlua_pushinteger (L, (lua_Integer) info->st_ino);\n}\n/* number of hard links to the file */\nstatic void push_st_nlink (lua_State *L, STAT_STRUCT *info) {\n\tlua_pushinteger (L, (lua_Integer)info->st_nlink);\n}\n/* user-id of owner */\nstatic void push_st_uid (lua_State *L, STAT_STRUCT *info) {\n\tlua_pushinteger (L, (lua_Integer)info->st_uid);\n}\n/* group-id of owner */\nstatic void push_st_gid (lua_State *L, STAT_STRUCT *info) {\n\tlua_pushinteger (L, (lua_Integer)info->st_gid);\n}\n/* device type, for special file inode */\nstatic void push_st_rdev (lua_State *L, STAT_STRUCT *info) {\n\tlua_pushinteger (L, (lua_Integer) info->st_rdev);\n}\n/* time of last access */\nstatic void push_st_atime (lua_State *L, STAT_STRUCT *info) {\n\tlua_pushinteger (L, (lua_Integer) info->st_atime);\n}\n/* time of last data modification */\nstatic void push_st_mtime (lua_State *L, STAT_STRUCT *info) {\n\tlua_pushinteger (L, (lua_Integer) info->st_mtime);\n}\n/* time of last file status change */\nstatic void push_st_ctime (lua_State *L, STAT_STRUCT *info) {\n\tlua_pushinteger (L, (lua_Integer) info->st_ctime);\n}\n/* file size, in bytes */\nstatic void push_st_size (lua_State *L, STAT_STRUCT *info) {\n\tlua_pushinteger (L, (lua_Integer)info->st_size);\n}\n\nstatic const char *perm2string (unsigned short mode) {\n\tstatic char perms[10] = \"---------\";\n\tint i;\n\tfor (i=0;i<9;i++) perms[i]='-';\n\tif (mode  & _S_IREAD)\n\t\t{ perms[0] = 'r'; perms[3] = 'r'; perms[6] = 'r'; }\n\tif (mode  & _S_IWRITE)\n\t\t{ perms[1] = 'w'; perms[4] = 'w'; perms[7] = 'w'; }\n\tif (mode  & _S_IEXEC)\n\t\t{ perms[2] = 'x'; perms[5] = 'x'; perms[8] = 'x'; }\n\treturn perms;\n}\n\n/* permssions string */\nstatic void push_st_perm (lua_State *L, STAT_STRUCT *info) {\n\tlua_pushstring (L, perm2string (info->st_mode));\n}\n\ntypedef void (*_push_function) (lua_State *L, STAT_STRUCT *info);\n\nstruct _stat_members {\n\tconst char *name;\n\t_push_function push;\n};\n\nstruct _stat_members members[] = {\n\t{ \"mode\",         push_st_mode },\n\t{ \"dev\",          push_st_dev },\n\t{ \"ino\",          push_st_ino },\n\t{ \"nlink\",        push_st_nlink },\n\t{ \"uid\",          push_st_uid },\n\t{ \"gid\",          push_st_gid },\n\t{ \"rdev\",         push_st_rdev },\n\t{ \"access\",       push_st_atime },\n\t{ \"modification\", push_st_mtime },\n\t{ \"change\",       push_st_ctime },\n\t{ \"size\",         push_st_size },\n\t{ \"permissions\",  push_st_perm },\n\t{ NULL, NULL }\n};\n\n/*\n** Get file or symbolic link information\n*/\nstatic int\nfile_info (lua_State *L) {\n\tSTAT_STRUCT info;\n\tsize_t sz;\n\tint i;\n\tconst char * utf8path = luaL_checklstring(L, 1, &sz);\n\twchar_t file[LONGPATH_MAX];\n\twindows_filename(L, utf8path, sz, file, LONGPATH_MAX);\n\n\tif (STAT_FUNC(file,\t&info))\t{\n\t\t\tlua_pushnil(L);\n\t\t\tlua_pushfstring(L, \"cannot obtain information from file\t'%s': %s\", file, strerror(errno));\n\t\t\tlua_pushinteger(L, errno);\n\t\t\treturn 3;\n\t}\n\tif (lua_isstring (L, 2)) {\n\t\t\tconst char *member = lua_tostring (L, 2);\n\t\t\tfor\t(i = 0;\tmembers[i].name; i++) {\n\t\t\t\t\tif (strcmp(members[i].name,\tmember)\t== 0) {\n\t\t\t\t\t\t\t/* push\tmember value and return\t*/\n\t\t\t\t\t\t\tmembers[i].push\t(L,\t&info);\n\t\t\t\t\t\t\treturn 1;\n\t\t\t\t\t}\n\t\t\t}\n\t\t\t/* member not found\t*/\n\t\t\treturn luaL_error(L, \"invalid attribute\tname '%s'\",\tmember);\n\t}\n\t/* creates a table if none is given, removes extra arguments */\n\tlua_settop(L, 2);\n\tif (!lua_istable (L, 2)) {\n\t\t\tlua_newtable (L);\n\t}\n\t/* stores all members in table on top of the stack */\n\tfor\t(i = 0;\tmembers[i].name; i++) {\n\t\t\tlua_pushstring (L, members[i].name);\n\t\t\tmembers[i].push\t(L,\t&info);\n\t\t\tlua_rawset (L, -3);\n\t}\n\treturn 1;\n}\n\nstatic int\nlrealpath(lua_State *L) {\n\tsize_t sz;\n\tconst char * pathname = luaL_checklstring(L, 1, &sz);\n\twchar_t winname[LONGPATH_MAX];\n\twchar_t fullname[LONGPATH_MAX];\n\twindows_filename(L, pathname, sz, winname, LONGPATH_MAX);\n\tDWORD r = GetFullPathNameW(winname, LONGPATH_MAX, fullname, NULL);\n\tif (r == 0) {\n\t\treturn error_return(L);\n\t}\n\tif (r > LONGPATH_MAX) {\n\t\treturn luaL_error(L, \"Invalid path %s\", pathname);\n\t}\n\tchar result[LONGPATH_MAX];\n\tint len = utf8_filename(L, fullname, r, result, LONGPATH_MAX);\n\tlua_pushlstring(L, result, len);\n\treturn 1;\n}\n\nstatic inline int\ncreate_dir_wchar_(const WCHAR *filenameW) {\n\tWIN32_FIND_DATAW FindFileData;\n\tHANDLE h = FindFirstFileW(filenameW, &FindFileData);\n\tif (h == INVALID_HANDLE_VALUE) {\n\t\t// create dir\n\t\tif (CreateDirectoryW(filenameW, NULL) == 0)\n\t\t\treturn -1;\n\t} else {\n\t\tif (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {\n\t\t\tFindClose(h);\n\t\t\t// dir exist\n\t\t} else {\n            FindClose(h);\n\t\t\t// not a dir\n\t\t\treturn 0;\n\t\t}\n\t}\n\treturn 1;\n}\n\nstatic int\nmkdir_utf8(const char *name) {\n\tWCHAR filenameW[FILENAME_MAX + 0x200 + 1];\n\tint n = MultiByteToWideChar(CP_UTF8,0,(const char*)name,-1,filenameW,FILENAME_MAX + 0x200);\n\tif (n == 0)\n\t\treturn -1;\n\treturn create_dir_wchar_(filenameW);\n}\n\nstatic int\npusherror(lua_State * L) {\n\tlua_pushnil(L);\n\tDWORD err = GetLastError();\n\tLPVOID lpMsgBuf;\n\n\tif (FormatMessageW(\n\t\tFORMAT_MESSAGE_ALLOCATE_BUFFER | \n\t\tFORMAT_MESSAGE_FROM_SYSTEM |\n\t\tFORMAT_MESSAGE_IGNORE_INSERTS,\n\t\tNULL,\n\t\terr,\n\t\t0,\n\t\t(LPWSTR) &lpMsgBuf,\n\t\t0, NULL) == 0) {\n\t\tlua_pushstring(L, \"FormatMessage failed\");\n    }\n\t\n\tchar errtext[1024] = \"unknown\";\n\tsize_t sz = wcslen((LPWSTR)lpMsgBuf);\n\n\tWideCharToMultiByte(CP_UTF8, 0, (LPWSTR)lpMsgBuf, sz, errtext, 1024, NULL, NULL);\n\n\tLocalFree(lpMsgBuf);\n\t\n\tlua_pushstring(L, errtext);\n\tlua_pushinteger(L, err);\n\treturn 3;\n}\n\n#else\n\n#define LONGPATH_MAX 4096\n\n#include <sys/stat.h>\n#include <dirent.h>\n#include <unistd.h>\n#include <errno.h>\n#include <string.h>\n#include <stdlib.h>\n#include <pwd.h>\n\n#define STAT_STRUCT struct stat\n#define STAT_FUNC stat\n\n#define DIR_METATABLE \"SOLUNA_DIR\"\n\nstruct dir_data {\n\tDIR* dir;\n\tint closed;\n};\n\nstatic void\nsystem_error(lua_State *L, int errcode) {\n\tlua_pushstring(L, strerror(errcode));\n}\n\nstatic int\nerror_return(lua_State *L) {\n\tlua_pushnil(L);\n\tsystem_error(L, errno);\n\treturn 2;\n}\n\nstatic int\ndir_iter(lua_State *L) {\n\tstruct dir_data *d = luaL_checkudata(L, 1, DIR_METATABLE);\n\tluaL_argcheck(L, d->closed == 0, 1, \"closed directory\");\n\tif (d->dir == NULL) {\n\t\t// no find found\n\t\td->closed = 1;\n\t\treturn 0;\n\t}\n\tif (lua_getuservalue(L, 1) == LUA_TSTRING) {\n\t\t// first time\n\t\tlua_pushnil(L);\n\t\tlua_setuservalue(L, 1);\n\t\treturn 1;\n\t} else {\n\t\tstruct dirent *entry = readdir(d->dir);\n\t\tif (entry) {\n\t\t\tlua_pushstring(L, entry->d_name);\n\t\t\treturn 1;\n\t\t} else {\n\t\t\tclosedir(d->dir);\n\t\t\td->dir = NULL;\n\t\t\td->closed = 1;\n\t\t\treturn 0;\n\t\t}\n\t}\n}\n\nstatic int\ndir_close(lua_State *L) {\n\tstruct dir_data *d = luaL_checkudata(L, 1, DIR_METATABLE);\n\tif (d->dir != NULL) {\n\t\tclosedir(d->dir);\n\t\td->dir = NULL;\n\t}\n\td->closed = 1;\n\treturn 0;\n}\n\nstatic int\nldir(lua_State *L) {\n\tsize_t sz;\n\tconst char * pathname = luaL_checklstring(L, 1, &sz);\n\tDIR* dir = opendir(pathname);\n\t\n\tlua_pushcfunction(L, dir_iter);\n\t\n\tif (dir == NULL) {\n\t\tif (errno == ENOENT) {\n\t\t\tstruct dir_data *d = lua_newuserdata(L, sizeof(*d));\n\t\t\td->dir = NULL;\n\t\t\td->closed = 0;\n\t\t} else {\n\t\t\tsystem_error(L, errno);\n\t\t\treturn lua_error(L);\n\t\t}\n\t} else {\n\t\tstruct dirent *entry = readdir(dir);\n\t\tstruct dir_data *d = lua_newuserdata(L, sizeof(*d));\n\t\td->dir = dir;\n\t\td->closed = 0;\n\t\tif (entry) {\n\t\t\tlua_pushstring(L, entry->d_name);\n\t\t\tlua_setuservalue(L, -2);\t// set firstname\n\t\t}\n\t}\n\n\tif (luaL_newmetatable(L, DIR_METATABLE)) {\n\t\tlua_pushvalue(L, -1);\n\t\tlua_setfield(L, -2, \"__index\");\n\n\t\tlua_pushcfunction(L, dir_iter);\n\t\tlua_setfield(L, -2, \"next\");\n\t\tlua_pushcfunction(L, dir_close);\n\t\tlua_setfield(L, -2, \"close\");\n\t\tlua_pushcfunction(L, dir_close);\n\t\tlua_setfield(L, -2, \"__gc\");\n\t}\n\tlua_setmetatable(L, -2);\n\treturn 2;\n}\n\nstatic int\nlpersonaldir(lua_State *L) {\n#if defined(__EMSCRIPTEN__)\n  lua_pushstring(L, \"/\");\n  return 1;\n#else\n\tstruct passwd *pw = getpwuid(getuid());\n\tif (pw && pw->pw_dir) {\n\t\tlua_pushstring(L, pw->pw_dir);\n\t\treturn 1;\n\t} else {\n\t\treturn error_return(L);\n\t}\n#endif\n}\n\nstatic int\nlcurrentdir(lua_State *L) {\n\tchar path[LONGPATH_MAX];\n\tif (getcwd(path, LONGPATH_MAX) == NULL) {\n\t\treturn error_return(L);\n\t}\n\tlua_pushstring(L, path);\n\treturn 1;\n}\n\nstatic int\nlchdir(lua_State *L) {\n\tsize_t sz;\n\tconst char * path = luaL_checklstring(L, 1, &sz);\n\tif (chdir(path) != 0) {\n\t\treturn error_return(L);\n\t}\n\tlua_pushboolean(L, 1);\n\treturn 1;\n}\n\nstatic const char *\nmode2string(mode_t mode) {\n\tif (S_ISREG(mode))\n\t\treturn \"file\";\n\telse if (S_ISDIR(mode))\n\t\treturn \"directory\";\n\telse if (S_ISLNK(mode))\n\t\treturn \"link\";\n\telse if (S_ISSOCK(mode))\n\t\treturn \"socket\";\n\telse if (S_ISFIFO(mode))\n\t\treturn \"named pipe\";\n\telse if (S_ISCHR(mode))\n\t\treturn \"char device\";\n\telse if (S_ISBLK(mode))\n\t\treturn \"block device\";\n\telse\n\t\treturn \"other\";\n}\n\n/* inode protection mode */\nstatic void push_st_mode(lua_State *L, STAT_STRUCT *info) {\n\tlua_pushstring(L, mode2string(info->st_mode));\n}\n/* device inode resides on */\nstatic void push_st_dev(lua_State *L, STAT_STRUCT *info) {\n\tlua_pushinteger(L, (lua_Integer) info->st_dev);\n}\n/* inode's number */\nstatic void push_st_ino(lua_State *L, STAT_STRUCT *info) {\n\tlua_pushinteger(L, (lua_Integer) info->st_ino);\n}\n/* number of hard links to the file */\nstatic void push_st_nlink(lua_State *L, STAT_STRUCT *info) {\n\tlua_pushinteger(L, (lua_Integer)info->st_nlink);\n}\n/* user-id of owner */\nstatic void push_st_uid(lua_State *L, STAT_STRUCT *info) {\n\tlua_pushinteger(L, (lua_Integer)info->st_uid);\n}\n/* group-id of owner */\nstatic void push_st_gid(lua_State *L, STAT_STRUCT *info) {\n\tlua_pushinteger(L, (lua_Integer)info->st_gid);\n}\n/* device type, for special file inode */\nstatic void push_st_rdev(lua_State *L, STAT_STRUCT *info) {\n\tlua_pushinteger(L, (lua_Integer) info->st_rdev);\n}\n/* time of last access */\nstatic void push_st_atime(lua_State *L, STAT_STRUCT *info) {\n\tlua_pushinteger(L, (lua_Integer) info->st_atime);\n}\n/* time of last data modification */\nstatic void push_st_mtime(lua_State *L, STAT_STRUCT *info) {\n\tlua_pushinteger(L, (lua_Integer) info->st_mtime);\n}\n/* time of last file status change */\nstatic void push_st_ctime(lua_State *L, STAT_STRUCT *info) {\n\tlua_pushinteger(L, (lua_Integer) info->st_ctime);\n}\n/* file size, in bytes */\nstatic void push_st_size(lua_State *L, STAT_STRUCT *info) {\n\tlua_pushinteger(L, (lua_Integer)info->st_size);\n}\n\nstatic const char *perm2string(mode_t mode) {\n\tstatic char perms[10] = \"---------\";\n\tint i;\n\tfor (i=0;i<9;i++) perms[i]='-';\n\t\n\tif (mode & S_IRUSR) perms[0] = 'r';\n\tif (mode & S_IWUSR) perms[1] = 'w';\n\tif (mode & S_IXUSR) perms[2] = 'x';\n\tif (mode & S_IRGRP) perms[3] = 'r';\n\tif (mode & S_IWGRP) perms[4] = 'w';\n\tif (mode & S_IXGRP) perms[5] = 'x';\n\tif (mode & S_IROTH) perms[6] = 'r';\n\tif (mode & S_IWOTH) perms[7] = 'w';\n\tif (mode & S_IXOTH) perms[8] = 'x';\n\t\n\treturn perms;\n}\n\n/* permissions string */\nstatic void push_st_perm(lua_State *L, STAT_STRUCT *info) {\n\tlua_pushstring(L, perm2string(info->st_mode));\n}\n\ntypedef void (*_push_function) (lua_State *L, STAT_STRUCT *info);\n\nstruct _stat_members {\n\tconst char *name;\n\t_push_function push;\n};\n\nstruct _stat_members members[] = {\n\t{ \"mode\",         push_st_mode },\n\t{ \"dev\",          push_st_dev },\n\t{ \"ino\",          push_st_ino },\n\t{ \"nlink\",        push_st_nlink },\n\t{ \"uid\",          push_st_uid },\n\t{ \"gid\",          push_st_gid },\n\t{ \"rdev\",         push_st_rdev },\n\t{ \"access\",       push_st_atime },\n\t{ \"modification\", push_st_mtime },\n\t{ \"change\",       push_st_ctime },\n\t{ \"size\",         push_st_size },\n\t{ \"permissions\",  push_st_perm },\n\t{ NULL, NULL }\n};\n\n/*\n** Get file or symbolic link information\n*/\nstatic int\nfile_info(lua_State *L) {\n\tSTAT_STRUCT info;\n\tsize_t sz;\n\tint i;\n\tconst char * path = luaL_checklstring(L, 1, &sz);\n\n\tif (STAT_FUNC(path, &info)) {\n\t\tlua_pushnil(L);\n\t\tlua_pushfstring(L, \"cannot obtain information from file '%s': %s\", path, strerror(errno));\n\t\tlua_pushinteger(L, errno);\n\t\treturn 3;\n\t}\n\tif (lua_isstring(L, 2)) {\n\t\tconst char *member = lua_tostring(L, 2);\n\t\tfor (i = 0; members[i].name; i++) {\n\t\t\tif (strcmp(members[i].name, member) == 0) {\n\t\t\t\t/* push member value and return */\n\t\t\t\tmembers[i].push(L, &info);\n\t\t\t\treturn 1;\n\t\t\t}\n\t\t}\n\t\t/* member not found */\n\t\treturn luaL_error(L, \"invalid attribute name '%s'\", member);\n\t}\n\t/* creates a table if none is given, removes extra arguments */\n\tlua_settop(L, 2);\n\tif (!lua_istable(L, 2)) {\n\t\tlua_newtable(L);\n\t}\n\t/* stores all members in table on top of the stack */\n\tfor (i = 0; members[i].name; i++) {\n\t\tlua_pushstring(L, members[i].name);\n\t\tmembers[i].push(L, &info);\n\t\tlua_rawset(L, -3);\n\t}\n\treturn 1;\n}\n\nstatic int\nlrealpath(lua_State *L) {\n\tsize_t sz;\n\tconst char * pathname = luaL_checklstring(L, 1, &sz);\n\tchar resolved[LONGPATH_MAX];\n\t\n\tif (realpath(pathname, resolved) == NULL) {\n\t\treturn error_return(L);\n\t}\n\t\n\tlua_pushstring(L, resolved);\n\treturn 1;\n}\n\n#define mkdir_utf8(path) (mkdir((path), \\\n    S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IXOTH))\n\t\n// todo: succ when dir exist\n\t\nstatic int\npusherror(lua_State * L) {\n\tlua_pushnil(L);\n\tlua_pushstring(L, strerror(errno));\n\tlua_pushinteger(L, errno);\n\treturn 3;\n}\n\n#endif\n\nstatic int\npushresult(lua_State * L, int res) {\n\tif (res == -1) {\n\t\treturn pusherror(L);\n\t} else if (res == 0) {\n\t\tlua_pushnil(L);\n\t\tlua_pushfstring(L, \"%s already exist\", lua_tostring(L, 1));\n\t\treturn 2;\n\t} else {\n\t\tlua_pushboolean(L, 1);\n\t\treturn 1;\n\t}\n}\n\nstatic int\nlmkdir(lua_State * L) {\n\tconst char *path = luaL_checkstring(L, 1);\n\treturn pushresult(L, mkdir_utf8(path));\n}\n\nint\nluaopen_localfs(lua_State *L) {\n\tluaL_checkversion(L);\n\tluaL_Reg l[] = {\n\t\t{ \"personaldir\" , lpersonaldir },\n\t\t{ \"dir\", ldir },\n\t\t{ \"currentdir\", lcurrentdir },\n\t\t{ \"chdir\", lchdir },\n\t\t{ \"attributes\", file_info },\t// the same with lfs, but support utf-8 filename\n\t\t{ \"realpath\", lrealpath },\n\t\t{ \"mkdir\", lmkdir },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\n\treturn 1;\n}\n"
  },
  {
    "path": "src/loginfo.h",
    "content": "#ifndef soluna_loginfo_h\n#define soluna_loginfo_h\n\n#include <stdint.h>\n\nstruct log_info {\n\tchar tag[64];\n\tuint32_t log_level;\n\tuint32_t log_item;\n\tuint32_t line_nr;\n\tchar message[256];\n\tconst char *filename;\t\n};\n\n#endif"
  },
  {
    "path": "src/lsha1.c",
    "content": "/*\nSHA-1 in C\nBy Steve Reid <sreid@sea-to-sky.net>\n100% Public\tDomain\n\n-----------------\nModified 7/98\nBy James H.\tBrown <jbrown@burgoyne.com>\nStill 100% Public Domain\n\nCorrected a\tproblem\twhich generated\timproper hash values on\t16 bit machines\nRoutine\tSHA1Update changed from\n\tvoid SHA1Update(SHA1_CTX* context, unsigned\tchar* data,\tunsigned int\nlen)\nto\n\tvoid SHA1Update(SHA1_CTX* context, unsigned\tchar* data,\tunsigned\nlong len)\n\nThe\t'len' parameter\twas\tdeclared an\tint\twhich works\tfine on\t32 bit machines.\nHowever, on\t16 bit machines\tan int is too small\tfor\tthe\tshifts being done\nagainst\nit.\t This caused the hash function to generate incorrect values\tif len was\ngreater\tthan 8191 (8K -\t1) due to the 'len << 3' on\tline 3 of SHA1Update().\n\nSince the file IO in main()\treads 16K at a time, any file 8K or\tlarger would\nbe guaranteed to generate the wrong\thash (e.g. Test\tVector #3, a million\n\"a\"s).\n\nI also changed the declaration of variables\ti &\tj in SHA1Update\tto\nunsigned long from unsigned\tint\tfor\tthe\tsame reason.\n\nThese changes should make no difference\tto any 32 bit implementations since\nan\nint\tand\ta long are the same\tsize in\tthose environments.\n\n--\nI also corrected a few compiler\twarnings generated by Borland C.\n1. Added #include <process.h> for exit() prototype\n2. Removed unused variable 'j' in SHA1Final\n3. Changed exit(0) to return(0)\tat end of main.\n\nALL\tchanges\tI made can be located by searching for comments\tcontaining 'JHB'\n-----------------\nModified 8/98\nBy Steve Reid <sreid@sea-to-sky.net>\nStill 100% public domain\n\n1- Removed #include\t<process.h>\tand\tused return() instead of exit()\n2- Fixed overwriting of\tfinalcount in SHA1Final() (discovered by Chris Hall)\n3- Changed email address from steve@edmweb.com to sreid@sea-to-sky.net\n\n-----------------\nModified 4/01\nBy Saul\tKravitz\t<Saul.Kravitz@celera.com>\nStill 100% PD\nModified to\trun\ton Compaq Alpha\thardware.\n\n-----------------\nModified 07/2002\nBy Ralph Giles <giles@ghostscript.com>\nStill 100% public domain\nmodified for use with stdint types,\tautoconf\ncode cleanup, removed attribution comments\nswitched SHA1Final() argument order\tfor\tconsistency\nuse\tSHA1_ prefix for public\tapi\nmove public\tapi\tto sha1.h\n\n-----------------\nModufiled 08/2014\nBy Cloud Wu <cloudwu@gmail.com>\nStill 100% PD\nLua binding\n*/\n\n/*\nTest Vectors (from FIPS\tPUB\t180-1)\n\"abc\"\n  A9993E36 4706816A\tBA3E2571 7850C26C 9CD0D89D\n\"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq\"\n  84983E44 1C3BD26E\tBAAE4AA1 F95129E5 E54670F1\nA million repetitions of \"a\"\n  34AA973C D4C4DAA4\tF61EEB2B DBAD2731 6534016F\n*/\n\n#include <stdio.h>\n#include <string.h>\n#include <stdint.h>\n \ntypedef struct {\n\tuint32_t state[5];\n\tuint32_t count[2];\n\tuint8_t  buffer[64];\n} SHA1_CTX;\n \n#define SHA1_DIGEST_SIZE 20\n\nstatic void\tSHA1_Transform(uint32_t\tstate[5], const\tuint8_t\tbuffer[64]);\n\n#define\trol(value, bits) (((value) << (bits)) |\t((value) >>\t(32\t- (bits))))\n\n/* blk0() and blk()\tperform\tthe\tinitial\texpand.\t*/\n/* I got the idea of expanding during the round\tfunction from SSLeay */\n/* FIXME: can we do\tthis in\tan endian-proof\tway? */\n#ifdef WORDS_BIGENDIAN\n#define\tblk0(i)\tblock.l[i]\n#else\n#define\tblk0(i)\t(block.l[i]\t= (rol(block.l[i],24)&0xFF00FF00) \\\n\t|(rol(block.l[i],8)&0x00FF00FF))\n#endif\n#define\tblk(i) (block.l[i&15] =\trol(block.l[(i+13)&15]^block.l[(i+8)&15] \\\n\t^block.l[(i+2)&15]^block.l[i&15],1))\n\n/* (R0+R1),\tR2,\tR3,\tR4 are the different operations\tused in\tSHA1 */\n#define\tR0(v,w,x,y,z,i)\tz+=((w&(x^y))^y)+blk0(i)+0x5A827999+rol(v,5);w=rol(w,30);\n#define\tR1(v,w,x,y,z,i)\tz+=((w&(x^y))^y)+blk(i)+0x5A827999+rol(v,5);w=rol(w,30);\n#define\tR2(v,w,x,y,z,i)\tz+=(w^x^y)+blk(i)+0x6ED9EBA1+rol(v,5);w=rol(w,30);\n#define\tR3(v,w,x,y,z,i)\tz+=(((w|x)&y)|(w&x))+blk(i)+0x8F1BBCDC+rol(v,5);w=rol(w,30);\n#define\tR4(v,w,x,y,z,i)\tz+=(w^x^y)+blk(i)+0xCA62C1D6+rol(v,5);w=rol(w,30);\n\n\n/* Hash\ta single 512-bit block.\tThis is\tthe\tcore of\tthe\talgorithm. */\nstatic void\tSHA1_Transform(uint32_t\tstate[5], const\tuint8_t\tbuffer[64])\n{\n\tuint32_t a,\tb, c, d, e;\n\ttypedef\tunion {\n\t\tuint8_t\tc[64];\n\t\tuint32_t l[16];\n\t} CHAR64LONG16;\n\tCHAR64LONG16 block;\n\n\tmemcpy(&block, buffer, 64);\n\n\t/* Copy\tcontext->state[] to\tworking\tvars */\n\ta =\tstate[0];\n\tb =\tstate[1];\n\tc =\tstate[2];\n\td =\tstate[3];\n\te =\tstate[4];\n\n\t/* 4 rounds\tof 20 operations each. Loop\tunrolled. */\n\tR0(a,b,c,d,e, 0); R0(e,a,b,c,d,\t1);\tR0(d,e,a,b,c, 2); R0(c,d,e,a,b,\t3);\n\tR0(b,c,d,e,a, 4); R0(a,b,c,d,e,\t5);\tR0(e,a,b,c,d, 6); R0(d,e,a,b,c,\t7);\n\tR0(c,d,e,a,b, 8); R0(b,c,d,e,a,\t9);\tR0(a,b,c,d,e,10); R0(e,a,b,c,d,11);\n\tR0(d,e,a,b,c,12); R0(c,d,e,a,b,13);\tR0(b,c,d,e,a,14); R0(a,b,c,d,e,15);\n\tR1(e,a,b,c,d,16); R1(d,e,a,b,c,17);\tR1(c,d,e,a,b,18); R1(b,c,d,e,a,19);\n\tR2(a,b,c,d,e,20); R2(e,a,b,c,d,21);\tR2(d,e,a,b,c,22); R2(c,d,e,a,b,23);\n\tR2(b,c,d,e,a,24); R2(a,b,c,d,e,25);\tR2(e,a,b,c,d,26); R2(d,e,a,b,c,27);\n\tR2(c,d,e,a,b,28); R2(b,c,d,e,a,29);\tR2(a,b,c,d,e,30); R2(e,a,b,c,d,31);\n\tR2(d,e,a,b,c,32); R2(c,d,e,a,b,33);\tR2(b,c,d,e,a,34); R2(a,b,c,d,e,35);\n\tR2(e,a,b,c,d,36); R2(d,e,a,b,c,37);\tR2(c,d,e,a,b,38); R2(b,c,d,e,a,39);\n\tR3(a,b,c,d,e,40); R3(e,a,b,c,d,41);\tR3(d,e,a,b,c,42); R3(c,d,e,a,b,43);\n\tR3(b,c,d,e,a,44); R3(a,b,c,d,e,45);\tR3(e,a,b,c,d,46); R3(d,e,a,b,c,47);\n\tR3(c,d,e,a,b,48); R3(b,c,d,e,a,49);\tR3(a,b,c,d,e,50); R3(e,a,b,c,d,51);\n\tR3(d,e,a,b,c,52); R3(c,d,e,a,b,53);\tR3(b,c,d,e,a,54); R3(a,b,c,d,e,55);\n\tR3(e,a,b,c,d,56); R3(d,e,a,b,c,57);\tR3(c,d,e,a,b,58); R3(b,c,d,e,a,59);\n\tR4(a,b,c,d,e,60); R4(e,a,b,c,d,61);\tR4(d,e,a,b,c,62); R4(c,d,e,a,b,63);\n\tR4(b,c,d,e,a,64); R4(a,b,c,d,e,65);\tR4(e,a,b,c,d,66); R4(d,e,a,b,c,67);\n\tR4(c,d,e,a,b,68); R4(b,c,d,e,a,69);\tR4(a,b,c,d,e,70); R4(e,a,b,c,d,71);\n\tR4(d,e,a,b,c,72); R4(c,d,e,a,b,73);\tR4(b,c,d,e,a,74); R4(a,b,c,d,e,75);\n\tR4(e,a,b,c,d,76); R4(d,e,a,b,c,77);\tR4(c,d,e,a,b,78); R4(b,c,d,e,a,79);\n\n\t/* Add the working vars\tback into context.state[] */\n\tstate[0] +=\ta;\n\tstate[1] +=\tb;\n\tstate[2] +=\tc;\n\tstate[3] +=\td;\n\tstate[4] +=\te;\n\n\t/* Wipe\tvariables */\n\ta =\tb =\tc =\td =\te =\t0;\n}\n\n\n/* SHA1Init\t- Initialize new context */\nstatic void sat_SHA1_Init(SHA1_CTX* context)\n{\n\t/* SHA1\tinitialization constants */\n\tcontext->state[0] =\t0x67452301;\n\tcontext->state[1] =\t0xEFCDAB89;\n\tcontext->state[2] =\t0x98BADCFE;\n\tcontext->state[3] =\t0x10325476;\n\tcontext->state[4] =\t0xC3D2E1F0;\n\tcontext->count[0] =\tcontext->count[1] =\t0;\n}\n\n\n/* Run your\tdata through this. */\nstatic void sat_SHA1_Update(SHA1_CTX* context,\tconst uint8_t* data, const size_t len)\n{\n\tsize_t i, j;\n\n#ifdef VERBOSE\n\tSHAPrintContext(context, \"before\");\n#endif\n\n\tj =\t(context->count[0] >> 3) & 63;\n\tif ((context->count[0] += len << 3)\t< (len << 3)) context->count[1]++;\n\tcontext->count[1] += (len >> 29);\n\tif ((j + len) >\t63)\t{\n\t\tmemcpy(&context->buffer[j],\tdata, (i = 64-j));\n\t\tSHA1_Transform(context->state, context->buffer);\n\t\tfor\t( ;\ti +\t63 < len; i\t+= 64) {\n\t\t\tSHA1_Transform(context->state, data\t+ i);\n\t\t}\n\t\tj =\t0;\n\t}\n\telse i = 0;\n\tmemcpy(&context->buffer[j],\t&data[i], len -\ti);\n\n#ifdef VERBOSE\n\tSHAPrintContext(context, \"after\t\");\n#endif\n}\n\n\n/* Add padding and return the message digest. */\nstatic void sat_SHA1_Final(SHA1_CTX* context, uint8_t digest[SHA1_DIGEST_SIZE])\n{\n\tuint32_t i;\n\tuint8_t\t finalcount[8];\n\n\tfor\t(i = 0;\ti <\t8; i++)\t{\n\t\tfinalcount[i] =\t(unsigned char)((context->count[(i >= 4\t? 0\t: 1)]\n\t\t >>\t((3-(i & 3)) * 8) )\t& 255);\t /*\tEndian independent */\n\t}\n\tsat_SHA1_Update(context, (uint8_t *)\"\\200\",\t1);\n\twhile ((context->count[0] &\t504) !=\t448) {\n\t\tsat_SHA1_Update(context, (uint8_t *)\"\\0\", 1);\n\t}\n\tsat_SHA1_Update(context, finalcount, 8);  /* Should\tcause a\tSHA1_Transform() */\n\tfor\t(i = 0;\ti <\tSHA1_DIGEST_SIZE; i++) {\n\t\tdigest[i] =\t(uint8_t)\n\t\t ((context->state[i>>2]\t>> ((3-(i &\t3))\t* 8) ) & 255);\n\t}\n\n\t/* Wipe\tvariables */\n\ti =\t0;\n\tmemset(context->buffer,\t0, 64);\n\tmemset(context->state, 0, 20);\n\tmemset(context->count, 0, 8);\n\tmemset(finalcount, 0, 8);\t/* SWR */\n}\n\n#include <lua.h>\n#include <lauxlib.h>\n\nint\nlsha1(lua_State *L) {\n\tsize_t sz = 0;\n\tconst uint8_t * buffer = (const uint8_t *)luaL_checklstring(L, 1, &sz);\n\tuint8_t digest[SHA1_DIGEST_SIZE];\n\tSHA1_CTX ctx;\n\tsat_SHA1_Init(&ctx);\n\tsat_SHA1_Update(&ctx, buffer, sz);\n\tsat_SHA1_Final(&ctx, digest);\n\tlua_pushlstring(L, (const char *)digest, SHA1_DIGEST_SIZE);\n\n\treturn 1;\n}\n\n#define BLOCKSIZE 64\n\nstatic inline void\nxor_key(uint8_t key[BLOCKSIZE], uint32_t xor_) {\n\tint i;\n\tfor (i=0;i<BLOCKSIZE;i+=sizeof(uint32_t)) {\n\t\tuint32_t * k = (uint32_t *)&key[i];\n\t\t*k ^= xor_;\n\t}\n}\n\nint\nlhmac_sha1(lua_State *L) {\n\tsize_t key_sz = 0;\n\tconst uint8_t * key = (const uint8_t *)luaL_checklstring(L, 1, &key_sz);\n\tsize_t text_sz = 0;\n\tconst uint8_t * text = (const uint8_t *)luaL_checklstring(L, 2, &text_sz);\n\tSHA1_CTX ctx1, ctx2;\n\tuint8_t digest1[SHA1_DIGEST_SIZE];\n\tuint8_t digest2[SHA1_DIGEST_SIZE];\n\tuint8_t rkey[BLOCKSIZE];\n\tmemset(rkey, 0, BLOCKSIZE);\n\n\tif (key_sz > BLOCKSIZE) {\n\t\tSHA1_CTX ctx;\n\t\tsat_SHA1_Init(&ctx);\n\t\tsat_SHA1_Update(&ctx, key, key_sz);\n\t\tsat_SHA1_Final(&ctx, rkey);\n\t\tkey_sz = SHA1_DIGEST_SIZE;\n\t} else {\n\t\tmemcpy(rkey, key, key_sz);\n\t}\n\n\txor_key(rkey, 0x5c5c5c5c);\n\tsat_SHA1_Init(&ctx1);\n\tsat_SHA1_Update(&ctx1, rkey, BLOCKSIZE);\n\n\txor_key(rkey, 0x5c5c5c5c ^ 0x36363636);\n\tsat_SHA1_Init(&ctx2);\n\tsat_SHA1_Update(&ctx2, rkey, BLOCKSIZE);\n\tsat_SHA1_Update(&ctx2, text, text_sz);\n\tsat_SHA1_Final(&ctx2, digest2);\n\n\tsat_SHA1_Update(&ctx1, digest2, SHA1_DIGEST_SIZE);\n\tsat_SHA1_Final(&ctx1, digest1);\n\n\tlua_pushlstring(L, (const char *)digest1, SHA1_DIGEST_SIZE);\n\n\treturn 1;\n}\n\n"
  },
  {
    "path": "src/luabuffer.h",
    "content": "#ifndef soluna_luabuffer_h\n#define soluna_luabuffer_h\n\n#include <stdint.h>\n#include <lua.h>\n#include <lauxlib.h>\n\nstatic inline uint8_t const *\nluaL_getbuffer(lua_State *L, size_t *sz) {\n\tuint8_t const * ret = NULL;\n\tswitch (lua_type(L, 1)) {\n\tcase LUA_TFUNCTION: {\n\t\tlua_pushvalue(L, 1);\n\t\tlua_call(L, 0, 3);\n\t\tret = (uint8_t const*)lua_touserdata(L, -3);\n\t\t*sz = (size_t)luaL_checkinteger(L, -2);\n\t\tlua_copy(L, -1, 1);\n\t\tint t = lua_type(L, 1);\n\t\tif (t == LUA_TUSERDATA || t == LUA_TTABLE)\n\t\t\tlua_toclose(L, 1);\n\t\tlua_pop(L, 3);\n\t\tbreak;\n\t}\n\tcase LUA_TSTRING:\n\t\tret = (uint8_t const *)luaL_checklstring(L, 1, sz);\n\t\tbreak;\n\tdefault:\n\t\tluaL_error(L, \"Invalid buffer type %s\", lua_typename(L, lua_type(L, 1)));\n\t}\n\treturn ret;\n}\n\n#endif\n"
  },
  {
    "path": "src/lualib/coroutine.lua",
    "content": "local co = require \"coroutine\"\n\nglobal assert, setmetatable, error\n\nlocal coroutine_create = co.create\nlocal coroutine_resume = co.resume\nlocal coroutine_close = co.close\nlocal coroutine_yield = co.yield\nlocal coroutine_status = co.status\n\nlocal coroutine = {}\n\ndo -- begin coroutine\n\tlocal ltask_coroutines = setmetatable({}, { __mode = \"kv\" })\n\t-- true : coroutine\n\t-- false : suspend\n\t-- nil : exit\n\n\tfunction coroutine.create(f)\n\t\tlocal co = coroutine_create(f)\n\t\tltask_coroutines[co] = true\n\t\treturn co\n\tend\n\t\n\tdo -- begin coroutine.resume\n\t\tlocal function unlock(co, ...)\n\t\t\tltask_coroutines[co] = true\n\t\t\treturn ...\n\t\tend\n\n\t\tlocal function ltask_yielding(co, ...)\n\t\t\tltask_coroutines[co] = false\n\t\t\treturn unlock(co, coroutine_resume(co, coroutine_yield(...)))\n\t\tend\n\n\t\tlocal function resume(co, ok, tag, ...)\n\t\t\tif not ok then\n\t\t\t\treturn ok, tag, ...\n\t\t\telseif coroutine_status(co) == \"dead\" then\n\t\t\t\t-- the main function exit\n\t\t\t\tltask_coroutines[co] = nil\n\t\t\t\treturn true, tag, ...\n\t\t\telseif tag == \"USER\" then\n\t\t\t\treturn true, ...\n\t\t\telse\n\t\t\t\t-- blocked in ltask framework, so raise the yielding message\n\t\t\t\treturn resume(co, ltask_yielding(co, tag, ...))\n\t\t\tend\n\t\tend\n\n\t\tfunction coroutine.resume(co, ...)\n\t\t\tlocal co_status = ltask_coroutines[co]\n\t\t\tif not co_status then\n\t\t\t\tif co_status == false then\n\t\t\t\t\t-- is running\n\t\t\t\t\treturn false, \"cannot resume a ltask coroutine suspend by ltask framework\"\n\t\t\t\tend\n\t\t\t\tif coroutine_status(co) == \"dead\" then\n\t\t\t\t\t-- always return false, \"cannot resume dead coroutine\"\n\t\t\t\t\treturn coroutine_resume(co, ...)\n\t\t\t\telse\n\t\t\t\t\treturn false, \"cannot resume none ltask coroutine\"\n\t\t\t\tend\n\t\t\tend\n\t\t\treturn resume(co, coroutine_resume(co, ...))\n\t\tend\t\n\tend -- end coroutine.resume\n\t\n\tfunction coroutine.status(co)\n\t\tlocal status = ltask_coroutines(co)\n\t\tif status == \"suspended\" then\n\t\t\tif ltask_coroutines[co] == false then\n\t\t\t\treturn \"blocked\"\n\t\t\telse\n\t\t\t\treturn \"suspended\"\n\t\t\tend\n\t\telse\n\t\t\treturn status\n\t\tend\n\tend\n\t\n\tfunction coroutine.yield(...)\n\t\treturn coroutine_yield(\"USER\", ...)\n\tend\n\n\tdo -- begin coroutine.wrap\n\n\t\tlocal function wrap_co(ok, ...)\n\t\t\tif ok then\n\t\t\t\treturn ...\n\t\t\telse\n\t\t\t\terror(...)\n\t\t\tend\n\t\tend\n\n\t\tfunction coroutine.wrap(f)\n\t\t\tlocal co = coroutine.create(function(...)\n\t\t\t\treturn f(...)\n\t\t\tend)\n\t\t\treturn function(...)\n\t\t\t\treturn wrap_co(coroutine.resume(co, ...))\n\t\t\tend\n\t\tend\n\n\tend\t-- end coroutine.wrap\n\n\tfunction coroutine.close(co)\n\t\tltask_coroutines[co] = nil\n\t\treturn coroutine_close(co)\n\tend\nend -- end corotuine\n\n_ENV.coroutine = coroutine\n\nreturn coroutine\n"
  },
  {
    "path": "src/lualib/fontmgr.lua",
    "content": "local ttf = require \"soluna.font.truetype\"\nlocal string = string\nlocal utf8 = utf8\nlocal table = table\nlocal debug = debug\n\nglobal pairs, ipairs, assert, rawget, setmetatable\n\nlocal MAXFONT <const> = 64\n\nlocal namelist = {}\n\nlocal CACHE = {}\n\nlocal function utf16toutf8(s)\n\tlocal surrogate\n\treturn (s:gsub(\"..\", function(utf16)\n\t\tlocal cp = string.unpack(\">H\", utf16)\n\t\tif (cp & 0xFC00) == 0xD800 then\n\t\t\tsurrogate = cp\n\t\t\treturn \"\"\n\t\telse\n\t\t\tif surrogate then\n\t\t\t\tcp = ((surrogate - 0xD800) << 10) + (cp - 0xDC00) + 0x10000\n\t\t\t\tsurrogate = nil\n\t\t\tend\n\t\t\treturn utf8.char(cp)\n\t\tend\n\tend))\nend\n\nlocal ids = {\n\tUNICODE = {\n\t\tid = 0,\n\t\tencoding = {\n\t\t\tUNICODE_1_0 = 0,\n\t\t\tUNICODE_1_1 = 1,\n\t\t\tISO_10646 = 2,\n\t\t\tUNICODE_2_0_BMP = 3,\n\t\t\tUNICODE_2_0_FULL = 4,\n\t\t},\n\t\tlang = {\n\t\t\tdefault = 0,\n\t\t\tENGLISH = 0,\n\t\t\tCHINESE = 1,\n\t\t\tFRENCH = 2,\n\t\t\tGERMAN = 3,\n\t\t\tJAPANESE = 4,\n\t\t\tKOREAN = 5,\n\t\t\tSPANISH = 6,\n\t\t\tITALIAN = 7,\n\t\t\tDUTCH = 8,\n\t\t\tSWEDISH = 9,\n\t\t\tRUSSIAN = 10,\n\t\t},\n\t},\n\tMICROSOFT = {\n\t\tid = 3,\n\t\tencoding = {\n\t\t\tUNICODE_BMP = 1,\n\t\t\tUNICODE_FULL = 10,\n\t\t},\n\t\tlang = {\n\t\t\tENGLISH     =0x0409,\n\t\t\tCHINESE     =0x0804,\n\t\t\tDUTCH       =0x0413,\n\t\t\tFRENCH      =0x040c,\n\t\t\tGERMAN      =0x0407,\n\t\t\tHEBREW      =0x040d,\n\t\t\tITALIAN     =0x0410,\n\t\t\tJAPANESE    =0x0411,\n\t\t\tKOREAN      =0x0412,\n\t\t\tRUSSIAN     =0x0419,\n\t\t\tSPANISH     =0x0409,\n\t\t\tSWEDISH     =0x041D,\n\t\t},\n\t},\n  MACINTOSH = {\n\t\tid = 1,\n\t\tencoding = {\n\t\t\tROMAN = 0,\n\t\t\tJAPANESE = 1,\n\t\t\tCHINESE_TRADITIONAL = 2,\n\t\t\tKOREAN = 3,\n\t\t\tARABIC = 4,\n\t\t\tHEBREW = 5,\n\t\t\tGREEK = 6,\n\t\t\tRUSSIAN = 7,\n\t\t\tRSYMBOL = 8,\n\t\t\tDEVANAGARI = 9,\n\t\t\tGURMUKHI = 10,\n\t\t\tGUJARATI = 11,\n\t\t\tORIYA = 12,\n\t\t\tBENGALI = 13,\n\t\t\tTAMIL = 14,\n\t\t\tTELUGU = 15,\n\t\t\tKANNADA = 16,\n\t\t\tMALAYALAM = 17,\n\t\t\tSINHALESE = 18,\n\t\t\tBURMESE = 19,\n\t\t\tKHMER = 20,\n\t\t\tTHAI = 21,\n\t\t\tLAOTIAN = 22,\n\t\t\tGEORGIAN = 23,\n\t\t\tARMENIAN = 24,\n\t\t\tCHINESE_SIMPLIFIED = 25,\n\t\t\tTIBETAN = 26,\n\t\t\tMONGOLIAN = 27,\n\t\t\tGEEZ = 28,\n\t\t\tSLAVIC = 29,\n\t\t\tVIETNAMESE = 30,\n\t\t\tSINDHI = 31,\n\t\t},\n\t\tlang = {\n\t\t\tENGLISH = 0,\n\t\t\tFRENCH = 1,\n\t\t\tGERMAN = 2,\n\t\t\tITALIAN = 3,\n\t\t\tDUTCH = 4,\n\t\t\tSWEDISH = 5,\n\t\t\tSPANISH = 6,\n\t\t\tDANISH = 7,\n\t\t\tPORTUGUESE = 8,\n\t\t\tNORWEGIAN = 9,\n\t\t\tHEBREW = 10,\n\t\t\tJAPANESE = 11,\n\t\t\tARABIC = 12,\n\t\t\tFINNISH = 13,\n\t\t\tGREEK = 14,\n\t\t\tICELANDIC = 15,\n\t\t\tMALTESE = 16,\n\t\t\tTURKISH = 17,\n\t\t\tCROATIAN = 18,\n\t\t\tCHINESE_TRADITIONAL = 19,\n\t\t\tURDU = 20,\n\t\t\tHINDI = 21,\n\t\t\tTHAI = 22,\n\t\t\tKOREAN = 23,\n\t\t\tLITHUANIAN = 24,\n\t\t\tPOLISH = 25,\n\t\t\tHUNGARIAN = 26,\n\t\t\tESTONIAN = 27,\n\t\t\tLATVIAN = 28,\n\t\t\tSAMI = 29,\n\t\t\tFAROESE = 30,\n\t\t\tFARSI = 31,\n\t\t\tRUSSIAN = 32,\n\t\t\tCHINESE_SIMPLIFIED = 33,\n\t\t\tFLEMISH = 34,\n\t\t\tIRISH_GAELIC = 35,\n\t\t\tALBANIAN = 36,\n\t\t\tROMANIAN = 37,\n\t\t\tCZECH = 38,\n\t\t\tSLOVAK = 39,\n\t\t\tSLOVENIAN = 40,\n\t\t\tYIDDISH = 41,\n\t\t\tSERBIAN = 42,\n\t\t\tMACEDONIAN = 43,\n\t\t\tBULGARIAN = 44,\n\t\t\tUKRAINIAN = 45,\n\t\t\tBYELORUSSIAN = 46,\n\t\t\tUZBEK = 47,\n\t\t\tKAZAKH = 48,\n\t\t\tAZERBAIJANI_CYRILLIC = 49,\n\t\t\tAZERBAIJANI_ARABIC = 50,\n\t\t\tARMENIAN = 51,\n\t\t\tGEORGIAN = 52,\n\t\t\tMOLDAVIAN = 53,\n\t\t\tKIRGHIZ = 54,\n\t\t\tTAJIKI = 55,\n\t\t\tTURKMEN = 56,\n\t\t\tMONGOLIAN = 57,\n\t\t\tMONGOLIAN_CYRILLIC = 58,\n\t\t\tPASHTO = 59,\n\t\t\tKURDISH = 60,\n\t\t\tKASHMIRI = 61,\n\t\t\tSINDHI = 62,\n\t\t\tTIBETAN = 63,\n\t\t\tNEPALI = 64,\n\t\t\tSANSKRIT = 65,\n\t\t\tMARATHI = 66,\n\t\t\tBENGALI = 67,\n\t\t\tASSAMESE = 68,\n\t\t\tGUJARATI = 69,\n\t\t\tPUNJABI = 70,\n\t\t\tORIYA = 71,\n\t\t\tMALAYALAM = 72,\n\t\t\tKANNADA = 73,\n\t\t\tTAMIL = 74,\n\t\t\tTELUGU = 75,\n\t\t\tSINHALESE = 76,\n\t\t\tBURMESE = 77,\n\t\t\tKHMER = 78,\n\t\t\tLAO = 79,\n\t\t\tVIETNAMESE = 80,\n\t\t\tINDONESIAN = 81,\n\t\t\tTAGALOG = 82,\n\t\t\tMALAY_ROMAN = 83,\n\t\t\tMALAY_ARABIC = 84,\n\t\t\tAMHARIC = 85,\n\t\t\tTIGRINYA = 86,\n\t\t\tGALLA = 87,\n\t\t\tSOMALI = 88,\n\t\t\tSWAHILI = 89,\n\t\t\tKINYARWANDA = 90,\n\t\t\tRUNDI = 91,\n\t\t\tNYANJA = 92,\n\t\t\tMALAGASY = 93,\n\t\t\tESPERANTO = 94,\n\t\t},\n\t},\n}\n\nlocal function import(fontdata)\n\tlocal index = 0\n\tlocal cache = {}\n\twhile true do\n\t\tfor _, obj in pairs(ids) do\n\t\t\tfor _, encoding_id in pairs(obj.encoding) do\n\t\t\t\tfor _, lang_id in pairs(obj.lang) do\n\t\t\t\t\tlocal fname, sname = ttf.namestring(fontdata, index, obj.id, encoding_id, lang_id)\n\t\t\t\t\tif fname then\n\t\t\t\t\t\tfname = utf16toutf8(fname)\n\t\t\t\t\t\tlocal fullname = fname\n\t\t\t\t\t\tfname = string.lower(fname)\n\t\t\t\t\t\tif sname then\n\t\t\t\t\t\t  sname = utf16toutf8(sname)\n\t\t\t\t\t\t  fullname = fullname .. \" \" .. sname\n\t\t\t\t\t\t  sname = string.lower(sname)\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif not cache[fullname] then\n\t\t\t\t\t\t\tcache[fullname] = true\n\t\t\t\t\t\t\ttable.insert(namelist, {\n\t\t\t\t\t\t\t\tfontdata = fontdata,\n\t\t\t\t\t\t\t\tindex = index,\n\t\t\t\t\t\t\t\tfamily = fname,\n\t\t\t\t\t\t\t\tsfamily = sname,\t-- sub family name\n\t\t\t\t\t\t\t\tname = string.lower(fullname),\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\tend\n\t\t\t\t\telseif fname == nil then\n\t\t\t\t\t\treturn\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\t\tindex = index + 1\n\tend\nend\n\nlocal FONT_ID = 0\nlocal function alloc_fontid()\n\tFONT_ID = FONT_ID + 1\n\tassert(FONT_ID <= MAXFONT)\n\treturn FONT_ID\nend\n\nlocal function matching(obj, name)\n\tif obj.family == name or obj.name == name then\n\t\treturn true\n\tend\nend\n\nlocal function fetch_name(nametable, name_)\n\tif name_ == \"\" and namelist[1] then\n\t\tname_ = namelist[1].name\n\t\tlocal id = rawget(nametable, name_)\n\t\tif id then\n\t\t\tnametable[\"\"] = id\n\t\t\treturn id\n\t\tend\n\tend\n\tlocal name = string.lower(name_)\n\tfor _, obj in ipairs(namelist) do\n\t\tif matching(obj, name) then\n\t\t\tif not obj.id then\n\t\t\t\tobj.id = alloc_fontid()\n\t\t\t\tCACHE[obj.id] = obj\n\t\t\tend\n\n\t\t\tlocal id = obj.id\n\t\t\tnametable[name_] = id\n\t\t\treturn id\n\t\tend\n\tend\nend\n\nsetmetatable(ttf.nametable, { __index = fetch_name })\n\nlocal function fetch_id(_, id)\n\tlocal obj = assert(CACHE[id])\n\treturn ttf.update(id, obj.fontdata, obj.index)\nend\n\nsetmetatable(ttf.idtable, { __index = fetch_id })\n\nlocal function enum_name(_, idx)\n\tlocal i = idx // 2\n\tlocal what = (idx % 2 == 1) and \"family\" or \"name\"\n\tlocal n = namelist[i+1]\n\tif not n then\n\t\treturn\n\telse\n\t\treturn n[what]\n\tend\nend\n\nsetmetatable(ttf.enum, { __index = enum_name })\n\ndebug.getregistry().TRUETYPE_IMPORT = import\n"
  },
  {
    "path": "src/lualib/icon.lua",
    "content": "local sdf = require \"soluna.image.sdf\"\nlocal datalist = require \"soluna.datalist\"\nlocal file = require \"soluna.file\"\nlocal mattext = require \"soluna.material.text\"\n\nglobal error, tostring, print\n\nlocal icon = {}\n\nfunction icon.bundle(filename)\n\tlocal path = filename:match \"(.*[/\\\\])[^/\\\\]+$\"\n\tlocal b = datalist.parse(file.load(filename))\n\tlocal names = {}\n\tlocal icons = {}\n\tlocal n = #b\n\tfor i = 1, n do\n\t\tlocal icon = b[i]\n\t\tnames[icon.name] = i - 1\n\t\tlocal src = file.load(path .. icon.image) or error (\"Open icon fail : \" .. tostring(icon.name))\n\t\tlocal img = sdf.load(src)\n\t\ticons[i] = img\n\tend\n\ticon.names = names\n\treturn sdf.bundle(icons)\nend\n\nfunction icon.symbol(name, size, color)\n\tlocal id = icon.names[name] or error \"No icon \" .. name\n\treturn mattext.char(id, 255, size, color)\nend\n\nreturn icon"
  },
  {
    "path": "src/lualib/initsetting.lua",
    "content": "local datalist = require \"soluna.datalist\"\nlocal source = require \"soluna.embedsource\"\nlocal lfs = require \"soluna.lfs\"\nlocal file = require \"soluna.file\"\n\nglobal type, error, pairs, assert, tonumber, print\n\nlocal S = {}\n\nlocal function patch(s, k, v)\n\tif type(k) == \"number\" then\n\t\t-- ignore\n\t\treturn\n\tend\n\tlocal branch, key = k:match \"^([^.]+)%.(.+)\"\n\tif branch then\n\t\tlocal tree = s[branch]\n\t\tif tree == nil then\n\t\t\ttree = {}\n\t\t\ts[branch] = tree\n\t\telseif type(tree) ~= \"table\" then\n\t\t\terror (\"Conflict setting key : \" .. k)\n\t\tend\n\t\ts = tree\n\t\tk = key\n\tend\n\t\n\tif type(v) == \"table\" then\n\t\tlocal orig_v = s[k]\n\t\tif orig_v == nil then\n\t\t\ts[k] = v\n\t\telseif type(orig_v) == \"table\" then\n\t\t\tfor sub_k,sub_v in pairs(v) do\n\t\t\t\tpatch(orig_v, sub_k, sub_v)\n\t\t\tend\n\t\telse\n\t\t\terror (\"Conflict setting key : \" .. k)\n\t\tend\n\telse\n\t\ts[k] = v\n\tend\nend\n\nlocal function settings_filename(filename, change_root)\n\tif filename then\n\t\tlocal realname = assert(lfs.realpath(filename))\n\t\tif change_root then\n\t\t\tlocal curpath, name = realname:match \"(.*)[/\\\\]([^/\\\\]+)$\"\n\t\t\tif curpath and name then\n\t\t\t\tlfs.chdir(curpath)\n\t\t\tend\n\t\t\treturn name\n\t\telse\n\t\t\treturn realname\n\t\tend\n\tend\n\tif file.exist \"main.game\" then\n\t\treturn \"main.game\"\n\tend\nend\n\nfunction S.init(args, change_root)\n\tlocal default_settings = datalist.parse(source.data.settingdefault)\n\tlocal realname = settings_filename(args[1], change_root)\n\tif realname then\n\t\tlocal data = file.load(realname) or error (\"Can't open \" .. realname)\n\t\tlocal game_settings = datalist.parse(data)\n\t\tfor k,v in pairs(game_settings) do\n\t\t\tpatch(default_settings, k,v)\n\t\tend\n\tend\n\tfor k,v in pairs(args) do\n\t\tif type(k) == \"string\" then\n\t\t\tif v == \"true\" then\n\t\t\t\tv = true\n\t\t\telseif v == \"false\" then\n\t\t\t\tv = false\n\t\t\telse\n\t\t\t\tv = tonumber(v) or v\n\t\t\tend\n\t\t\tpatch(default_settings, k,v)\n\t\tend\n\tend\n\treturn default_settings\nend\n\nreturn S\n"
  },
  {
    "path": "src/lualib/layout.lua",
    "content": "local yoga = require \"soluna.layout.yoga\"\nlocal datalist = require \"soluna.datalist\"\nlocal file = require \"soluna.file\"\nlocal table = table\n\nglobal next, error, assert, type, setmetatable, pairs\n\nlocal layout = {}\n\nlocal document = {}\nlocal element = {} ; element.__index = element\n\nfunction document:__gc()\n\tlocal root = self._root\t-- root yoga object\n\tif root then\n\t\tyoga.node_free(root)\n\t\tself._root = nil\n\tend\n\tself._yoga = nil\t-- yoga objects for elements\n\tself._list = nil\t-- image/text element lists\n\tself._element = nil\t-- elements can be update\nend\n\nfunction document:__index(id)\n\treturn self._element[id]\nend\n\nfunction document:__tostring()\n\treturn \"[document]\"\nend\n\nfunction document:__pairs()\n\treturn next, self._element\nend\n\nfunction element:__tostring()\n\treturn \"[element:\"..self._id..\"]\"\nend\n\nfunction element:__newindex(key, value)\n\tlocal _yoga = self._document._yoga\n\tlocal cobj = (_yoga and _yoga[self._id]) or error (\"No id : \" .. self._id)\n\tyoga.node_set(cobj, key, value)\nend\n\n-- update attr\nfunction element:update(attr)\n\tlocal _yoga = self._document._yoga\n\tlocal cobj = (_yoga and _yoga[self._id]) or error (\"No id : \" .. self._id)\n\tyoga.node_set(cobj, attr)\nend\n\nfunction element:get()\n\tlocal _yoga = self._document._yoga\n\tlocal cobj = (_yoga and _yoga[self._id]) or error (\"No id : \" .. self._id)\n\treturn yoga.node_get(cobj)\nend\n\nfunction element:attribs()\n\tlocal _yoga = self._document._yoga\n\tlocal cobj = (_yoga and _yoga[self._id]) or error (\"No id : \" .. self._id)\n\treturn _yoga[cobj]\nend\n\ndo\n\tlocal function parse_node(v, scripts)\n\t\tlocal attr = {}\n\t\tlocal content = {}\n\t\tlocal n = 1\n\t\tfor i = 1, #v, 2 do\n\t\t\tlocal name = v[i]\n\t\t\tlocal value = v[i+1]\n\t\t\tif name == \"children\" then\n\t\t\t\tlocal c = scripts(value)\n\t\t\t\tlocal len = #c\n\t\t\t\tassert(type(c) == \"table\" and len % 2 == 0)\n\t\t\t\ttable.move(c, 1, len, n, content)\n\t\t\t\tn = n + len\n\t\t\telseif type(value) == \"table\" then\n\t\t\t\tcontent[n] = name\n\t\t\t\tcontent[n+1] = value\n\t\t\t\tn = n + 2\n\t\t\telse\n\t\t\t\tattr[name] = value\n\t\t\tend\n\t\tend\n\t\tif n == 1 then\n\t\t\tcontent = nil\n\t\tend\n\t\treturn content, attr\n\tend\n\t\n\tlocal function new_element(doc, cobj, attr)\n\t\tyoga.node_set(cobj, attr)\n\t\tlocal id = attr.id\n\t\tif id then\n\t\t\tif doc._element[id] then\n\t\t\t\terror (id .. \" exist\")\n\t\t\tend\n\t\t\tlocal elem = { _document = doc, _id = id }\n\t\t\tdoc._element[id] = setmetatable(elem, element)\n\t\t\tdoc._yoga[id] = cobj\n\t\tend\n\t\t\n\t\tif attr.image or attr.text or attr.background or attr.region then\n\t\t\tlocal obj = {}\n\t\t\tfor k,v in pairs(attr) do\n\t\t\t\tobj[k] = v\n\t\t\tend\n\t\t\tdoc._yoga[obj] = cobj\n\t\t\tdoc._yoga[cobj] = obj\n\t\t\tdoc._list[#doc._list + 1] = obj\n\t\tend\n\tend\n\n\tlocal function add_children(doc, parent, list, scripts)\n\t\tfor i = 1, #list, 2 do\n\t\t\tlocal name = list[i]\t-- ignore\n\t\t\tlocal content, attr = parse_node(list[i+1], scripts)\n\t\t\tlocal cobj = yoga.node_new(parent)\n\t\t\tnew_element(doc, cobj, attr)\n\t\t\tif content then\n\t\t\t\tadd_children(doc, cobj, content, scripts)\n\t\t\tend\n\t\tend\n\tend\n\n\tfunction layout.load(filename_or_list, scripts)\n\t\tlocal list\n\t\tif type(filename_or_list) == \"string\" then\n\t\t\tlist = datalist.parse_list(file.load(filename_or_list))\n\t\telse\n\t\t\tlist = filename_or_list\n\t\tend\n\t\tlocal doc = {\n\t\t\t_root = yoga.node_new(),\n\t\t\t_yoga = {},\n\t\t\t_list = {},\n\t\t\t_element = {},\n\t\t}\n\t\t\n\t\tlocal children, attr = parse_node(list, scripts)\n\t\tnew_element(doc, doc._root, attr)\n\t\tif children then\n\t\t\tadd_children(doc, doc._root, children, scripts)\n\t\tend\n\n\t\treturn setmetatable(doc, document)\n\tend\n\t\n\tfunction layout.calc(doc)\n\t\tyoga.node_calc(doc._root)\n\t\tlocal list = doc._list\n\t\tlocal yogaobj = doc._yoga\n\t\tfor i = 1, #list do\n\t\t\tlocal obj = list[i]\n\t\t\tlocal cobj = yogaobj[obj]\n\t\t\tdo local _ENV = obj\n\t\t\t\tglobal x, y, w, h\n\t\t\t\tx,y,w,h = yoga.node_get(cobj)\n\t\t\tend\n\t\tend\n\t\tlocal _,_,w,h = yoga.node_get(doc._root)\n\t\tlist.width = w\n\t\tlist.height = h\n\t\treturn list\n\tend\nend\n\nreturn layout\n"
  },
  {
    "path": "src/lualib/main.lua",
    "content": "local package = package\nlocal table = table\n\nglobal load, require, assert, select, error, tostring, print, type\n\nlocal init_func_temp = [=[\n\tlocal name, service_path = ...\n\tlocal embedsource = require \"soluna.embedsource\"\n\tlocal file = require \"soluna.file\"\n\tpackage.path = [[${lua_path}]]\n\tpackage.cpath = [[${lua_cpath}]]\n\tlocal zipfile = [[${zipfile}]]\n\tif zipfile == \"\" then\n\t\tzipfile = nil\n\tend\n\t_G.print_r = load(embedsource.runtime.print_r(), \"@src/lualib/print_r.lua\")()\n\tlocal packageloader = load(embedsource.runtime.packageloader(), \"@src/lualib/packageloader.lua\")\n\tpackageloader(zipfile)\n\tlocal function embedloader(name)\n\t\tlocal ename\n\t\tif name == \"soluna\" then\n\t\t\tename = \"soluna\"\n\t\telse\n\t\t\tename = name:match \"^soluna%.(.*)\"\n\t\tend\n\t\tif ename then\n\t\t\tlocal code = embedsource.lib[ename]\n\t\t\tif code then\n\t\t\t\treturn function()\n\t\t\t\t\tlocal srcname = \"src/lualib/\"..ename..\".lua\"\n\t\t\t\t\tlocal f = load(code(), \"@\" .. srcname)\n\t\t\t\t\treturn f(ename, srcname)\n\t\t\t\tend\n\t\t\tend\n\t\t\treturn \"no embed soluna.\" .. ename\n\t\tend\n\tend\n\tpackage.searchers[#package.searchers+1] = embedloader\n\tlocal extlua = require \"soluna.extlua\"\n\tif extlua.searcher() then\t-- has preload libs\n\t\tpackage.searchers[#package.searchers+1] = extlua.searcher\n\tend\n\tlocal embedcode = embedsource.service[name]\n\tif embedcode then\n\t\treturn load(embedcode(),\"=(\"..name..\")\")\n\tend\n\tlocal filename, err = file.searchpath(name, service_path or \"${service_path}\")\n\tif not filename then\n\t\treturn nil, err\n\tend\n\treturn load(file.load(filename), \"@\"..filename)\n]=]\n\nlocal api = {}\n\nlocal function start(config)\n\tlocal boot = require \"ltask.bootstrap\"\n\tlocal mqueue = require \"ltask.mqueue\"\n\tlocal embedsource = require \"soluna.embedsource\"\n\tlocal soluna_app = require \"soluna.app\"\n\t-- set callback message handler\n\tlocal root_config = {\n\t\tbootstrap = config.bootstrap,\n\t\tservice_source = embedsource.runtime.service(),\n\t\tservice_chunkname = \"@3rd/ltask/lualib/service.lua\",\n\t\tinitfunc = init_func_temp:gsub(\"%$%{([^}]*)%}\", {\n\t\t\tlua_path = package.path,\n\t\t\tlua_cpath = package.cpath,\n\t\t\tservice_path = config.service_path or \"\",\n\t\t\tzipfile = config.args.zipfile or \"\",\n\t\t}),\n\t}\n\n\ttable.insert(root_config.bootstrap, {\n\t\tname = \"start\",\n\t\targs = {\n\t\t\tconfig.args,\n\t\t},\n\t})\n\n\tboot.init_socket()\n\tlocal bootstrap = load(embedsource.runtime.bootstrap(), \"@3rd/ltask/lualib/bootstrap.lua\")()\n\tlocal core = config.core or {}\n\tcore.external_queue = core.external_queue or 4096\n\tlocal ctx = bootstrap.start {\n\t\tcore = core,\n\t\troot = root_config,\n\t\troot_initfunc = root_config.initfunc,\n\t\tmainthread = config.mainthread,\n\t}\n\t-- wait for INIT_EVENT, see start.lua\n\tboot.mainthread_wait()\n\tlocal sender, sender_ud = bootstrap.external_sender(ctx)\n\tlocal c_sendmessage = require \"soluna.app\".sendmessage\n\tlocal function send_message(...)\n\t\treturn c_sendmessage(sender, sender_ud, ...)\n\tend\n\tlocal logger, logger_ud = bootstrap.log_sender(ctx)\n\tlocal unpackevent = assert(soluna_app.unpackevent)\n\tlocal appmsg_queue = mqueue.new(128)\n\tlocal recvmsg = mqueue.recv\n\t\n\tlocal appmsg = {}\n\t\n\tfunction appmsg.set_title(text)\n\t\tsoluna_app.set_window_title(text)\n\tend\n\n\tfunction appmsg.set_icon(data)\n\t\tsoluna_app.set_icon(data)\n\tend\n\t\n\tlocal function do_appmsg(what, ...)\n\t\tlocal f = appmsg[what] or error (\"Unknown app message \" .. tostring(what))\n\t\tf(...)\n\tend\n\t\n\tlocal function dispatch_appmsg(v)\n\t\twhile v do\n\t\t\tdo_appmsg(boot.unpack_remove(v))\n\t\t\tv = recvmsg(appmsg_queue, appmsg)\n\t\tend\n\tend\n\treturn {\n\t\tsend_log = logger,\n\t\tsend_log_ud = logger_ud,\n\t\tmqueue = appmsg_queue,\n\t\tcleanup = function()\n\t\t\twhile not send_message \"cleanup\" do end\n\t\t\tbootstrap.wait(ctx)\n\t\t\tapi.deinit()\n\t\t\tmqueue.delete(appmsg_queue)\n\t\t\tappmsg_queue = nil\n\t\tend,\n\t\tframe = function(count)\n\t\t\tlocal v = recvmsg(appmsg_queue)\n\t\t\tif v then\n\t\t\t\tdispatch_appmsg(v)\n\t\t\tend\n\t\t\tif send_message(\"frame\", count) then\n\t\t\t\tboot.mainthread_wait()\n\t\t\tend\n\t\tend,\n\t\tevent = function(ev)\n\t\t\tsend_message(unpackevent(ev))\n\t\tend,\n\t}\nend\n\nlocal args = ... or {}\n\nfor i = 2, select(\"#\", ...) do\n\targs[i-1] = select(i, ...)\nend\n\nif args.path then\n\tpackage.path = args.path\nend\n\nif args.cpath then\n\tpackage.cpath = args.cpath\nend\n\nlocal audio_device\n\nfunction api.start(app)\n\tapp.audio_device = audio_device\n\targs.app = app\n\treturn start {\n\t\targs = args,\n\t\tcore = {\n\t\t\tdebuglog = \"=\", -- stdout\n\t\t},\n\t\tbootstrap = {\n\t\t\t{\n\t\t\t\tname = \"timer\",\n\t\t\t\tunique = true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname = \"log\",\n\t\t\t\tunique = true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname = \"loader\",\n\t\t\t\tunique = true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname = \"audio\",\n\t\t\t\tunique = true,\n\t\t\t},\n\t\t},\n\t}\nend\n\nlocal function preload_ext(list, entry_name)\n\tif list == nil then\n\t\treturn\n\tend\n\tif type(list) == \"string\" then\n\t\tlist = { list }\n\tend\n\tfor i = 1, #list do\n\t\tlocal name = list[i]\n\t\tlocal f = package.loadlib(package.searchpath(name, package.cpath), entry_name) or error (\"Can't load extlua \" .. name)\n\t\tlist[i] = f\n\tend\n\tlocal extlua = require \"soluna.extlua\"\n\textlua.preload(list)\nend\n\nfunction api.init(desc)\n\t-- todo : settings\n\tlocal zipfile = args[1] or args.zipfile or \"main.zip\"\n\tlocal embedsource = require \"soluna.embedsource\"\n\tlocal packageloader = load(embedsource.runtime.packageloader(), \"@src/lualib/packageloader.lua\")\n\tif packageloader(zipfile) then\n\t\targs.zipfile = zipfile\n\t\tif zipfile == args[1] then\n\t\t\ttable.remove(args, 1)\n\t\tend\n\tend\n\tlocal initsetting = load(embedsource.lib.initsetting, \"@3rd/ltask/lualib/initsetting.lua\")()\n\tlocal settings = initsetting.init(args)\n\tpreload_ext(settings.extlua_preload, settings.extlua_entry)\n\tlocal soluna_app = require \"soluna.app\"\n\tsoluna_app.init_desc(desc, settings)\n\tlocal audio = require \"soluna.audio\"\n\taudio.device, audio_device = audio.init()\nend\n\nfunction api.deinit()\n\tif audio_device then\n\t\tlocal audio = require \"soluna.audio\"\n\t\taudio.deinit(audio_device)\n\t\taudio_device = nil\n\tend\nend\n\nreturn api\n"
  },
  {
    "path": "src/lualib/packageloader.lua",
    "content": "local file = require \"soluna.file\"\nlocal zip = require \"soluna.zip\"\nlocal lfs = require \"soluna.lfs\"\n\nlocal package = package\nlocal string = string\nlocal io = io\n\nglobal load, print, setmetatable, table, type, tostring, ipairs, require, error, assert\n\nlocal dir_sep, temp_sep, temp_marker = package.config:match \"(.)\\n(.)\\n(.)\"\nlocal temp_pat = \"[^\"..temp_sep..\"]+\"\n\nlocal function load_zips(zipnames)\n\tif zipnames == nil then\n\t\treturn\n\tend\n\tlocal n = 0\n\tlocal r = {}\n\tfor fullname in zipnames:gmatch \"[^:;]+\" do\n\t\tlocal name, root = fullname:match \"(.-)@(.*)\"\n\t\tif name then\n\t\t\troot = root .. \"/\"\n\t\telse\n\t\t\tname = fullname\n\t\tend\n\t\tlocal zf = zip.open(name, \"r\")\n\t\tif not zf then\n--\t\t\tprint(\"Can't open patch\", name)\n\t\telse\n--\t\t\tprint(\"Load patch\", name)\n\t\t\tn = n + 1\n\t\t\tr[n] = { zip = zf, root = root, name = name }\n\t\tend\n\tend\n\tr.n = n\n\tif n > 0 then\n\t\treturn r\n\telse\n--\t\tprint(\"No zip, use local files\")\n\tend\nend\n\nlocal zipfile = load_zips(...)\nlocal file_load = file.load\nlocal file_exist = file.exist\n\nif zipfile then\n\tlocal function find_file(cache, fullname)\n\t\tlocal name = fullname:match \"%./(.*)\" or fullname\n\t\tfor i = zipfile.n, 1, -1 do\n\t\t\tlocal root = zipfile[i].root\n\t\t\tlocal name_in_zip\n\t\t\tif root then\n\t\t\t\tlocal n = #root\n\t\t\t\tif name:sub(1, n) == root then\n\t\t\t\t\tname_in_zip = name:sub(n+1)\n\t\t\t\tend\n\t\t\telse\n\t\t\t\tname_in_zip = name\n\t\t\tend\n\t\t\tlocal zf = zipfile[i].zip\n\t\t\tif name_in_zip and zf:exist(name_in_zip) then\n\t\t\t\tcache[name] = function()\n\t\t\t\t\treturn zf:readfile(name_in_zip)\n\t\t\t\tend\n--\t\t\t\tprint(name, \"in zipfile\", i)\n\t\t\t\treturn cache[name]\n\t\t\tend\n\t\tend\n\tend\n\tlocal list\n\tlocal names_cache = setmetatable({}, { __index = find_file})\n\n\tfunction file_load(name)\n\t\tlocal loader = names_cache[name]\n\t\treturn loader and loader()\n\tend\n\tfunction file_exist(name)\n\t\treturn names_cache[name] ~= nil\n\tend\n\tfile.local_load = file.load\n\tfile.local_exist = file.exist\n\tfile.load = file_load\n\tfile.ziplist = function () return zip.list(zipfile) end\n\tfile.exist = file_exist\n\tlocal function gen_list()\n\t\tlocal tmp = {}\n\t\tlocal r = {}\n\t\tlocal n = 1\n\t\tfor i = zipfile.n, 1, -1 do\n\t\t\tlocal flist = zipfile[i].zip:list()\n\t\t\tlocal root = zipfile[i].root\n\t\t\tfor j = 1, #flist do\n\t\t\t\tlocal name = flist[j]\n\t\t\t\tif root then\n\t\t\t\t\tname = root and root .. name\n\t\t\t\tend\n\t\t\t\tif tmp[name] == nil then\n\t\t\t\t\ttmp[name] = true\n\t\t\t\t\t-- todo : add path of name\n\t\t\t\tend\n\t\t\t\tr[n] = name\n\t\t\t\tn = n + 1\n\t\t\tend\n\t\tend\n\t\ttable.sort(r)\n\t\treturn r\n\tend\n\tfunction file.dir(root)\n\t\tlist = list or gen_list()\n\t\troot = root:gsub(\"[^/]$\", \"%0/\")\n\t\tlocal iter = 1\n\t\tlocal n = #list\n\t\tlocal root_n = #root\n\t\tlocal last\n\t\treturn function()\n\t\t\twhile iter <= n do\n\t\t\t\tlocal t = list[iter]\n\t\t\t\titer = iter + 1\n\t\t\t\tif t:sub(1, root_n) == root then\n\t\t\t\t\tlocal sname = t:sub(root_n+1):match \"[^/]+\"\n\t\t\t\t\tif sname ~= last then\n\t\t\t\t\t\tlast = sname\n\t\t\t\t\t\treturn sname\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\n\tfunction file.attributes(fullname)\n\t\tlist = list or gen_list()\n\t\tlocal pathname = fullname .. \"/\"\n\t\tlocal pathn = #pathname\n\t\tfor i = 1, #list do\n\t\t\tlocal t = list[i]\n\t\t\tif fullname == t then\n\t\t\t\treturn \"file\"\n\t\t\telseif t:sub(1, pathn) == pathname then\n\t\t\t\treturn \"directory\"\n\t\t\tend\n\t\tend\n\tend\n\tfunction file.searchpath(name, path)\n\t\tlocal cname = name:gsub(\"%.\", \"/\")\n\t\tfor temp in path:gmatch(temp_pat) do\n\t\t\tlocal fullname = temp:gsub(temp_marker, cname)\n\t\t\tif dir_sep ~= '/' then\n\t\t\t\tfullname = fullname:gsub(dir_sep, \"/\")\n\t\t\tend\n\t\t\tif file_exist(fullname) then\n\t\t\t\treturn fullname\n\t\t\tend\n\t\tend\n\tend\nelse\n\tfile.dir = lfs.dir\n\tfile.attributes = lfs.attributes\n\tfile.local_load = file.load\n\tfile.local_exist = file.exist\n\tfile.searchpath = package.searchpath\nend\n\nlocal function fileload(name, fullname)\n\tlocal s, err = file_load(fullname)\n\tlocal f = assert(load(s, \"@\"..fullname))\n\treturn f(name, fullname)\nend\n\nlocal function search_file(name)\n\tlocal cname = name:gsub(\"%.\", \"/\")\n\tfor temp in package.path:gmatch(temp_pat) do\n\t\tlocal fullname = temp:gsub(temp_marker, cname)\n\t\tif dir_sep ~= '/' then\n\t\t\tfullname = fullname:gsub(dir_sep, \"/\")\n\t\tend\n\t\tif file_exist(fullname) then\n\t\t\treturn fileload, fullname\n\t\tend\n\tend\n\treturn \"No package : \" .. name\nend\n\npackage.searchers[2] = search_file\n\nreturn zipfile"
  },
  {
    "path": "src/lualib/print_r.lua",
    "content": "local ltask = require \"ltask\"\nlocal log_info = ltask.log.info\nlocal table = table\nlocal math = math\n\nglobal pairs, tostring, type, assert, pcall, error, select\n\nlocal function keys(o)\n\tlocal len = #o\n\tlocal skeys = {}\n\tlocal ukeys = {}\n\tlocal n = 1\n\tfor k,v in pairs(o) do\n\t\tlocal nk = math.tointeger(k)\n\t\tif nk == nil or nk <= 0 or nk > len then\n\t\t\tlocal sk = tostring(k)\n\t\t\tif type(k) == \"string\" then\n\t\t\t\tskeys[n] = k; n = n + 1\n\t\t\telse\n\t\t\t\tukeys[k] = true\n\t\t\tend\n\t\tend\n\tend\n\ttable.sort(skeys)\n\tfor k in pairs(ukeys) do\n\t\tskeys[n] = k; n = n + 1\n\tend\n\treturn skeys\nend\n\nlocal function try_no_circular(o)\n\tlocal cache = {}\n\tlocal function seri_no_circular(o)\n\t\tassert(cache[o] == nil, cache)\n\t\tif type(o) == \"table\" then\n\t\t\tlocal result = { \"{\" }\n\t\t\tlocal n = 2\n\t\t\tcache[o] = true\n\t\t\tfor i = 1, #o do\n\t\t\t\tlocal v = o[i]\n\t\t\t\tresult[n] = seri_no_circular(v); n = n + 1\n\t\t\t\tresult[n] = \" \"; n = n + 1\n\t\t\tend\n\t\t\tlocal key = keys(o)\n\t\t\tfor i = 1, #key do\n\t\t\t\tlocal k = key[i]\n\t\t\t\tlocal v = seri_no_circular(o[k])\n\t\t\t\tresult[n] = seri_no_circular(k); n = n + 1\n\t\t\t\tresult[n] = \":\"; n = n + 1\n\t\t\t\tresult[n] = v; n = n + 1\n\t\t\t\tresult[n] = \" \"; n = n + 1\n\t\t\tend\n\t\t\tresult[n] = \"}\"\n\t\t\treturn table.concat(result)\n\t\telse\n\t\t\treturn tostring(o)\n\t\tend\n\tend\n\t\n\tlocal ok, r = pcall(seri_no_circular, o)\n\tif not ok then\n\t\tif r == cache then\n\t\t\treturn\n\t\telse\n\t\t\terror(r)\n\t\tend\n\tend\n\treturn r\nend\n\nlocal function mark_circular(o)\n\tlocal cache = {}\n\tlocal n = 1\n\tlocal function mark(o)\n\t\tif type(o) == \"table\" then\n\t\t\tlocal v = cache[o]\n\t\t\tif v == nil then\n\t\t\t\tcache[o] = false\n\t\t\telse\n\t\t\t\tif v == false then\n\t\t\t\t\tcache[o] = n; n = n + 1\n\t\t\t\tend\n\t\t\t\treturn\n\t\t\tend\n\t\t\tfor k,v in pairs(o) do\n\t\t\t\tmark(k)\n\t\t\t\tmark(v)\n\t\t\tend\n\t\tend\n\tend\n\tmark(o)\n\tfor k,v in pairs(cache) do\n\t\tif not v then\n\t\t\tcache[k] = nil\n\t\tend\n\tend\n\treturn cache\nend\n\nlocal function seri_circular(o)\n\tlocal cache = mark_circular(o)\n\tlocal function seri_object(o)\n\t\tif type(o) ~= \"table\" then\n\t\t\treturn tostring(o)\n\t\tend\n\t\tlocal result\n\t\tlocal s = cache[o]\n\t\tif s then\n\t\t\tif type(s) == \"number\" then\n\t\t\t\tresult = { \"#\"..s..\"{\" }\n\t\t\t\tcache[o] = \"[#\"..s..\"]\"\n\t\t\telse\n\t\t\t\treturn s\n\t\t\tend\n\t\telse\n\t\t\tresult = { \"{\" }\n\t\tend\n\t\t\n\t\tlocal n = 2\n\t\tfor i = 1, #o do\n\t\t\tlocal v = o[i]\n\t\t\tresult[n] = seri_object(v); n = n + 1\n\t\t\tresult[n] = \" \"; n = n + 1\n\t\tend\n\t\tlocal key = keys(o)\n\t\tfor i = 1, #key do\n\t\t\tlocal k = key[i]\n\t\t\tlocal v = seri_object(o[k])\n\t\t\tresult[n] = seri_object(k); n = n + 1\n\t\t\tresult[n] = \":\"; n = n + 1\n\t\t\tresult[n] = v; n = n + 1\n\t\t\tresult[n] = \" \"; n = n + 1\n\t\tend\n\t\tresult[n] = \"}\"\n\t\treturn table.concat(result)\n\tend\n\treturn seri_object(o)\nend\n\nlocal function seri(o)\n\treturn try_no_circular(o) or seri_circular(o)\nend\n\nlocal function print_r(...)\n\tlocal len = select(\"#\", ...)\n\tlocal str\n\tif len == 1 then\n\t\tstr = seri(...)\n\telse\n\t\tlocal r = {}\n\t\tlocal n = 1\n\t\tfor i = 1, len do\n\t\t\tlocal o = select(i, ...)\n\t\t\tr[n] = seri(o); n = n + 1\n\t\tend\n\t\tstr = table.concat(r, \"\\t\")\n\tend\n\tlocal n = #str\n\tif n > 1024 then\n\t\tstr = str:sub(1, 1024) .. \"[...\" .. n .. \"]\"\n\tend\n\tltask.pushlog(ltask.pack(\"print\", str))\nend\n\nreturn print_r"
  },
  {
    "path": "src/lualib/soluna.lua",
    "content": "local ltask = require \"ltask\"\nlocal app = require \"soluna.app\"\nlocal mqueue = require \"ltask.mqueue\"\n\nglobal require, error, string, assert, package, setmetatable, tostring\n\nlocal soluna = {\n\tplatform = app.platform\n}\n\nfunction soluna.gamepad_init()\n\tlocal gamepad = require \"soluna.gamepad\"\n\tlocal state = {}\n\tsoluna.gamepad = state\n\tlocal gs = ltask.uniqueservice \"gamepad\"\n\tlocal S = ltask.dispatch()\n\n\tfunction S._gamepad_update()\n\t\tgamepad.update(state)\n\tend\n\n\tltask.call(gs, \"register\", ltask.self(), \"_gamepad_update\")\n\n\treturn state\nend\n\nlocal settings\nfunction soluna.settings()\n\tif settings == nil then\n\t\tlocal s = ltask.queryservice \"settings\"\n\t\tsettings = ltask.call(s, \"get\")\n\tend\n\treturn settings\nend\n\nfunction soluna.set_window_title(text)\n\tmqueue.send(app.mqueue(), ltask.pack(\"set_title\", text))\nend\n\nfunction soluna.set_icon(data)\n\tmqueue.send(app.mqueue(), ltask.pack(\"set_icon\", data))\nend\n\nlocal function recursion_mkdir(root, path)\n\tlocal lfs = require \"soluna.lfs\"\n\tfor p in path:gmatch \"[^/\\\\]+\" do\n\t\troot = root .. \"/\" .. p\n\t\tlfs.mkdir(root)\n\tend\n\treturn (root:gsub(\"[^/\\\\]$\", \"%0/\"))\nend\n\nfunction soluna.gamedir(name)\n\tif name == nil then\n\t\tsettings = settings and soluna.settings()\n\t\tname = settings.project or error \"missing project name in settings\"\n\tend\n\tlocal lfs = require \"soluna.lfs\"\n\tlocal path\n\tif soluna.platform == \"windows\" then\n\t\tpath = \"My Games/\"\n\telseif soluna.platform == \"macos\" or soluna.platform == \"linux\" then\n\t\tpath = \".local/share/\"\n\telseif soluna.platform == \"wasm\" then\n\t\tpath = \"persistent/games/\"\n\telse\n\t\terror \"TODO: support none windows\"\n\tend\n\tpath = path .. name\n\treturn recursion_mkdir(lfs.personaldir(), path)\nend\n\nfunction soluna.load_sprites(filename)\n\tlocal render = ltask.uniqueservice \"render\"\n\tlocal sprites = ltask.call(render, \"load_sprites\", filename)\n\treturn sprites\nend\n\nlocal audio_service\n\nlocal voice_index = {}\nlocal voice_mt = { __index = voice_index }\n\nfunction voice_index:stop(fade_seconds)\n\treturn ltask.call(audio_service, \"voice_stop\", self.id, fade_seconds)\nend\n\nfunction voice_index:playing()\n\treturn ltask.call(audio_service, \"voice_playing\", self.id)\nend\n\nfunction voice_index:set_volume(volume)\n\treturn ltask.call(audio_service, \"voice_set_volume\", self.id, volume)\nend\n\nfunction voice_index:set_pan(pan)\n\treturn ltask.call(audio_service, \"voice_set_pan\", self.id, pan)\nend\n\nfunction voice_index:set_pitch(pitch)\n\treturn ltask.call(audio_service, \"voice_set_pitch\", self.id, pitch)\nend\n\nfunction voice_index:set_loop(loop)\n\treturn ltask.call(audio_service, \"voice_set_loop\", self.id, loop)\nend\n\nfunction voice_index:seek(seconds)\n\treturn ltask.call(audio_service, \"voice_seek\", self.id, seconds)\nend\n\nfunction voice_index:tell()\n\treturn ltask.call(audio_service, \"voice_tell\", self.id)\nend\n\nlocal bus_index = {}\nlocal bus_mt = { __index = bus_index }\n\nfunction bus_index:set_volume(volume)\n\treturn ltask.call(audio_service, \"bus_set_volume\", self.name, volume)\nend\n\nfunction soluna.load_sounds(filename)\n\taudio_service = audio_service or ltask.uniqueservice \"audio\"\n\tltask.call(audio_service, \"init\", filename)\nend\n\nfunction soluna.play_sound(name, opts)\n\tlocal id, err = ltask.call(audio_service, \"play_sound\", name, opts)\n\tif not id then\n\t\treturn nil, err\n\tend\n\treturn setmetatable({ id = id }, voice_mt)\nend\n\nfunction soluna.audio_bus(name)\n\tif not ltask.call(audio_service, \"has_bus\", name) then\n\t\treturn nil, \"Unknown audio bus \" .. tostring(name)\n\tend\n\treturn setmetatable({ name = name }, bus_mt)\nend\n\nfunction soluna.preload(spr)\n\tlocal loader = ltask.uniqueservice \"loader\"\n\tif #spr == 0 then\n\t\tltask.call(loader, \"preload\", spr.filename, spr.content, spr.w, spr.h)\n\telse\n\t\tlocal async = ltask.async()\n\t\tfor i = 1, #spr do\n\t\t\tlocal s = spr[i]\n\t\t\tasync:request(loader, \"preload\", s.filename, s.content, s.w, s.h)\n\t\tend\n\t\tasync:wait()\n\tend\nend\n\nlocal function version()\n\tlocal api, hash = app.version()\n\tsoluna.version_api = api\n\treturn string.format(\"%03x\", api) .. hash:sub(1, 7)\nend\n\nsoluna.version = version()\n\nreturn soluna\n"
  },
  {
    "path": "src/lualib/spritebundle.lua",
    "content": "local image = require \"soluna.image\"\nlocal file = require \"soluna.file\"\nlocal datalist = require \"soluna.datalist\"\n\nglobal type, tonumber, error, assert, ipairs, print\n\nlocal M = {}\n\nlocal function load_bundle(filename)\n\tlocal b = datalist.parse(file.load(filename))\n\treturn b\nend\n\nlocal function crop_(item, c)\n\tlocal x = item.cx\n\tlocal y = item.cy\n\tlocal w = item.cw\n\tlocal h = item.ch\n\t\n\tlocal offx = item.x or 0\n\tlocal offy = item.y or 0\n\tif offx < 0 then\n\t\toffx = - c.w * offx // 1 | 0\n\tend\n\tif offy < 0 then\n\t\toffy = - c.h * offy // 1 | 0\n\tend\n\tlocal cx, cy, cw, ch = image.crop(c.data, c.w, c.h, x, y, w, h)\n\toffx = offx - cx\n\toffy = offy - cy\n\t\n\titem.x = offx\n\titem.y = offy\n\titem.cx = cx + (x or 0)\n\titem.cy = cy + (y or 0)\n\titem.cw = cw\n\titem.ch = ch\nend\n\nlocal function unpack_size(size)\n\tif type(size) == \"number\" then\n\t\treturn size, size\n\telse\n\t\tlocal x, y = size:match \"(%d+)[xX*](%d+)\"\n\t\treturn tonumber(x), tonumber(y)\n\tend\nend\n\nlocal function crop(item, filecache)\n\tlocal c = filecache[item.filename]\n\tif c == nil then\n\t\terror(\"No file : \" .. item.filename)\n\tend\n\tlocal number = item.number\n\tif number then\n\t\t-- multi sprites\n\t\tlocal cw, ch = unpack_size(assert(item.size))\n\t\tlocal gap = item.gap\n\t\tlocal gap_x = 0\n\t\tlocal gap_y = 0\n\t\tif gap then\n\t\t\tgap_x, gap_y = unpack_size(gap)\n\t\tend\n\t\tlocal cx = 0\n\t\tlocal cy = 0\n\t\tlocal col = 1\n\t\tlocal row\n\t\tif type(number) == \"number\" then\n\t\t\trow = number\n\t\telse\n\t\t\trow, col = unpack_size(number)\n\t\tend\n\t\tlocal count = 1\n\t\tlocal offx = item.x\n\t\tlocal offy = item.y\n\t\tgap_x = gap_x + cw\n\t\tgap_y = gap_y + ch\n\t\tlocal filename = item.filename\n\t\tfor i = 1, col do\n\t\t\tcx = 0\n\t\t\tfor j = 1, row do\n\t\t\t\tlocal s = { cx = cx, cy = cy, cw = cw, ch = ch , x = offx , y = offy, filename = filename }\n\t\t\t\titem[count] = s\n\t\t\t\tcrop_(s, c)\n\t\t\t\tcount = count + 1\n\t\t\t\tcx = cx + gap_x\n\t\t\tend\n\t\t\tcy = cy + gap_y\n\t\tend\n\telse\n\t\tcrop_(item, c)\n\tend\nend\n\nlocal function load_list(filecache, v, path)\n\tfor idx, item in ipairs(v) do\n\t\tlocal fname = item.filename or \"Need filename for item \" .. idx\n\t\tif path then\n\t\t\titem.filename = path .. fname\n\t\tend\n\t\tcrop(item, filecache)\n\tend\nend\n\nfunction M.load(filecache, filename, path)\n\tlocal v\n\tif type(filename) == \"table\" then\n\t\tv = filename\n\telse\n\t\tpath = path or filename:match \"(.*[/\\\\])[^/\\\\]+$\"\n\t\tv = load_bundle(filename)\n\tend\n\tload_list(filecache, v, path)\n\treturn v\nend\n\nfunction M.loadimage(filecache, filename)\n\tlocal content = file.load(filename)\n\tif not content then\n\t\tif not filecache.__missing[filename] then\n\t\t\tfilecache.__missing[filename] = true\n\t\t\tprint(\"Missing file : \" .. filename)\n\t\tend\n\t\treturn\n\tend\n\tlocal load = image.load\n\tif filename:find \"%.alpha%.\" then\n\t\tload = image.load_alpha\n\tend\n\tlocal data, w, h = load(content)\n\tif data == nil then\n\t\tif not filecache.__missing[filename] then\n\t\t\tfilecache.__missing[filename] = true\n\t\t\tprint(\"Invalid image : \" .. filename .. \"(\" .. w .. \")\")\n\t\tend\n\t\treturn\n\tend\n\tlocal r = { data = data, w = w, h = h }\n\tfilecache[filename] = r\n\treturn r\nend\n\nreturn M\n"
  },
  {
    "path": "src/lualib/text.lua",
    "content": "local font = require \"soluna.font\"\nlocal icon = require \"soluna.icon\"\nglobal setmetatable, print, tonumber\n\nlocal text = {}\n\nlocal bundle_data\n\nfunction text.init(bundle)\n\tbundle_data = icon.bundle(bundle)\t-- prevent gc to collect bundle_data\n\tfont.import_icon(bundle_data)\nend\n\nlocal colors = {\n\tred = \"[FF0000]\",\n\tgreen = \"[00FF00]\",\n\tblue = \"[0000FF]\",\n\twhite = \"[FFFFFF]\",\n\tblack = \"[000000]\",\n\taqua = \"[00FFFF]\",\n\tyellow = \"[FFFF00]\",\n\tpink = \"[FF00FF]\",\n\tgray = \"[808080]\",\n\tbracket = \"[bracket]\",\n}\n\nlocal function user_color(self, name)\n\tif name:byte() == 99 then\t-- 'c'\n\t\tlocal cvalue = name:sub(2)\n\t\tlocal c = tonumber(cvalue, 16)\n\t\tif c then\n\t\t\tlocal cname = \"[\" .. cvalue .. \"]\"\n\t\t\tself[name] = cname\n\t\t\treturn cname\n\t\tend\n\tend\nend\n\nsetmetatable(colors, { __index = user_color })\n\nlocal function icon_id(name)\n\tlocal cname = colors[name]\n\tif cname then\n\t\treturn cname\n\tend\n\tlocal id = icon.names[name]\n\tif not id then\n\t\treturn \"[\"..name..\"]\"\n\tend\n\treturn \"[i\"..id..\"]\"\nend\n\nlocal function convert(tbl, key)\n\tlocal escape = key:gsub(\"%[%[\", \"[bracket]\")\n\tlocal value = escape:gsub(\"%[(%w+)%]\", icon_id)\n\tif escape ~= key then\n\t\tvalue = value:gsub(\"%[bracket%]\", \"[[\")\n\telseif value == key then\n\t\t-- uniforming long string\n\t\tvalue = key\n\tend\n\ttbl[key]=value\n\treturn value\nend\n\ntext.convert = setmetatable({}, { __mode = \"kv\", __index = convert })\n\nreturn text\n"
  },
  {
    "path": "src/lualib/util.lua",
    "content": "local table = table\n\nglobal setmetatable\n\nlocal util = {}\n\nlocal func_chain = {}; func_chain.__index = func_chain\n\nfunction func_chain:add(f)\n\ttable.insert(self, f)\nend\n\nfunction func_chain:__call()\n\tfor i = 1, #self do\n\t\tlocal f = self[i]\n\t\tf()\n\tend\nend\n\nfunction util.func_chain()\n\treturn setmetatable({}, func_chain)\nend\n\nreturn util\n"
  },
  {
    "path": "src/luamods.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n\nint luaopen_ltask(lua_State *L);\nint luaopen_ltask_root(lua_State *L);\nint luaopen_ltask_bootstrap(lua_State *L);\nint luaopen_ltask_mqueue(lua_State *L);\nint luaopen_embedsource(lua_State *L);\nint luaopen_appmessage(lua_State *L);\nint luaopen_applog(lua_State *L);\nint luaopen_image(lua_State *L);\nint luaopen_render(lua_State *L);\nint luaopen_spritemgr(lua_State *L);\nint luaopen_datalist(lua_State *L);\nint luaopen_soluna_file(lua_State *L);\nint luaopen_font_truetype(lua_State *L);\nint luaopen_font_manager(lua_State *L);\nint luaopen_font(lua_State *L);\nint luaopen_drawmgr(lua_State *L);\nint luaopen_material_default(lua_State *L);\nint luaopen_material_text(lua_State *L);\nint luaopen_material_quad(lua_State *L);\nint luaopen_material_mask(lua_State *L);\nint luaopen_material_blit(lua_State *L);\nint luaopen_soluna_app(lua_State *L);\nint luaopen_font_system(lua_State *L);\nint luaopen_gamepad_device(lua_State *L);\nint luaopen_gamepad(lua_State *L);\nint luaopen_localfs(lua_State *L);\nint luaopen_image_sdf(lua_State *L);\nint luaopen_layout_yoga(lua_State *L);\nint luaopen_url(lua_State *L);\nint luaopen_skynet_crypt(lua_State *L);\nint luaopen_zip(lua_State *L);\nint luaopen_extlua(lua_State *L);\nint luaopen_soluna_audio(lua_State *L);\n\nvoid soluna_embed(lua_State* L) {\n    static const luaL_Reg modules[] = {\n\t\t{ \"ltask\", luaopen_ltask},\n\t\t{ \"ltask.root\", luaopen_ltask_root},\n\t\t{ \"ltask.bootstrap\", luaopen_ltask_bootstrap},\n\t\t{ \"ltask.mqueue\", luaopen_ltask_mqueue},\n\t\t{ \"soluna.app\", luaopen_soluna_app },\n\t\t{ \"soluna.embedsource\", luaopen_embedsource},\n\t\t{ \"soluna.log\", luaopen_applog },\n\t\t{ \"soluna.image\", luaopen_image },\n\t\t{ \"soluna.render\", luaopen_render },\n\t\t{ \"soluna.spritemgr\", luaopen_spritemgr },\n\t\t{ \"soluna.drawmgr\", luaopen_drawmgr },\n\t\t{ \"soluna.material.default\", luaopen_material_default },\n\t\t{ \"soluna.material.text\", luaopen_material_text },\n\t\t{ \"soluna.material.quad\", luaopen_material_quad },\n\t\t{ \"soluna.material.mask\", luaopen_material_mask },\n\t\t{ \"soluna.material.blit\", luaopen_material_blit },\n\t\t{ \"soluna.datalist\", luaopen_datalist },\n\t\t{ \"soluna.file\", luaopen_soluna_file },\n\t\t{ \"soluna.font\", luaopen_font },\n\t\t{ \"soluna.font.truetype\", luaopen_font_truetype },\n\t\t{ \"soluna.font.manager\", luaopen_font_manager },\n\t\t{ \"soluna.font.system\", luaopen_font_system },\n\t\t{ \"soluna.gamepad\", luaopen_gamepad },\n\t\t{ \"soluna.gamepad.device\", luaopen_gamepad_device },\n\t\t{ \"soluna.lfs\", luaopen_localfs },\n\t\t{ \"soluna.image.sdf\", luaopen_image_sdf },\n\t\t{ \"soluna.layout.yoga\", luaopen_layout_yoga },\n\t\t{ \"soluna.url\", luaopen_url },\n\t\t{ \"soluna.crypt\", luaopen_skynet_crypt },\n\t\t{ \"soluna.zip\", luaopen_zip },\n\t\t{ \"soluna.extlua\", luaopen_extlua },\n\t\t{ \"soluna.audio\", luaopen_soluna_audio },\n\t\t{ NULL, NULL },\n    };\n\n    const luaL_Reg *lib;\n    luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_PRELOAD_TABLE);\n    for (lib = modules; lib->func; lib++) {\n        lua_pushcfunction(L, lib->func);\n        lua_setfield(L, -2, lib->name);\n    }\n    lua_pop(L, 1);\n}\n"
  },
  {
    "path": "src/luayoga.c",
    "content": "#define LUA_LIB\n\n#include <lua.h>\n#include <lauxlib.h>\n#include <stdlib.h>\n#include <string.h>\n#include \"yoga/Yoga.h\"\n\n#define FlexDirection 1\n#define Justify 2\n#define Align 4\n#define Wrap 8\n#define Display 16\n#define PositionType 32\n\n#define ENUM(x, what) { YG##x##ToString(YG##x##what), (x) << 16 | (YG##x##what) },\n\nstruct enum_string {\n\tconst char * name;\n\tint value;\n};\n\nstruct set_number {\n\tvoid (*set)(YGNodeRef node, float width);\n\tvoid (*setPercent)(YGNodeRef node, float width);\n\tvoid (*setAuto)(YGNodeRef node);\n\tvoid (*setMaxContent)(YGNodeRef node);\n\tvoid (*setFitContent)(YGNodeRef node);\n\tvoid (*setStretch)(YGNodeRef node);\n};\n\nstruct set_edge_number {\n\tvoid (*set)(YGNodeRef node, YGEdge edge, float v);\n\tvoid (*setPercent)(YGNodeRef node, YGEdge edge, float v);\n\tvoid (*setAuto)(YGNodeRef node, YGEdge edge);\n};\n\nstruct set_two_number {\n\tvoid (*set)(YGNodeRef node, YGGutter gutter, float v);\n\tvoid (*setPercent)(YGNodeRef node, YGGutter gutter, float v);\n};\n\nstatic int\nlnodeNew(lua_State *L) {\n\tYGNodeRef node = YGNodeNew();\n\tif (lua_islightuserdata(L, 1)) {\n\t\tYGNodeRef parent = lua_touserdata(L, 1);\n\t\tsize_t n = YGNodeGetChildCount(parent);\n\t\tYGNodeInsertChild(parent, node, n);\n\t}\n\tlua_pushlightuserdata(L, node);\n\treturn 1;\n}\n\nstatic int\nlnodeFree(lua_State *L) {\n\tYGNodeRef node = lua_touserdata(L, 1);\n\tYGNodeFreeRecursive(node);\n\treturn 0;\n}\n\nstatic int\nlnodeCalc(lua_State *L) {\n\tYGNodeRef node = lua_touserdata(L, 1);\n\tYGNodeCalculateLayout(node, YGUndefined, YGUndefined, YGDirectionLTR);\n\treturn 0;\n}\n\nstruct pos {\n\tfloat x;\n\tfloat y;\n};\n\nstatic void\nget_pos(struct pos *p, YGNodeRef node) {\n\tp->x = 0;\n\tp->y = 0;\n\t\n\twhile (node) {\n\t\tp->x += YGNodeLayoutGetLeft(node);\n\t\tp->y += YGNodeLayoutGetTop(node);\n\t\tnode = YGNodeGetParent(node);\n\t}\n}\n\nstatic int\nlnodeGet(lua_State *L) {\n\tYGNodeRef node = lua_touserdata(L, 1);\n\tstruct pos p;\n\tget_pos(&p, node);\n\tfloat r[] = {\n\t\tp.x,\n\t\tp.y,\n\t\tYGNodeLayoutGetWidth(node),\n\t\tYGNodeLayoutGetHeight(node)\n\t};\n\tint i;\n\tfor (i=0;i<4;i++) {\n\t\tlua_pushnumber(L, r[i]);\n\t}\n\treturn 4;\n}\n\ntypedef void (*setfunc)(lua_State *L, YGNodeRef node);\n\nstatic inline int\nis_whitespace(char c) {\n\treturn c =='\\0' || c == ' ' || c == '\\t';\n}\n\nstatic void\nsetNumberString(lua_State *L, YGNodeRef node, const char *v, const struct set_number *setter) {\n\tchar* endptr = NULL;\n\tfloat number = strtof(v, &endptr);\n\tif (*endptr == '%') {\n\t\tsetter->setPercent(node, number);\n\t} else if (is_whitespace(*endptr)) {\n\t\tsetter->set(node, number);\n\t} else if (setter->setAuto && strcmp(v, \"auto\") == 0) {\n\t\tsetter->setAuto(node);\n\t} else if (strcmp(v, \"stretch\") == 0) {\n\t\tsetter->setStretch(node);\n\t} else if (strcmp(v, \"max-content\") == 0) {\n\t\tsetter->setMaxContent(node);\n\t} else if (strcmp(v, \"fit-content\") == 0) {\n\t\tsetter->setFitContent(node);\n\t} else {\n\t\tluaL_error(L, \"Invalid number %s\", v);\n\t}\n}\n\nstatic void\nsetNumber(lua_State *L, YGNodeRef node, const struct set_number *setter) {\n\tif (lua_type(L, -1) == LUA_TNUMBER) {\n\t\tfloat v = lua_tonumber(L, -1);\n\t\tsetter->set(node, v);\n\t} else {\n\t\tconst char * v = luaL_checkstring(L, -1);\n\t\tsetNumberString(L, node, v, setter);\n\t}\n}\n\nstatic void\nlsetWidth(lua_State *L, YGNodeRef node) {\n\tstatic const struct set_number setter = {\n\t\tYGNodeStyleSetWidth,\n\t\tYGNodeStyleSetWidthPercent,\n\t\tYGNodeStyleSetWidthAuto,\n\t\tYGNodeStyleSetWidthMaxContent,\n\t\tYGNodeStyleSetWidthFitContent,\n\t};\n\tsetNumber(L, node, &setter);\n}\n\nstatic void\nlsetMinWidth(lua_State *L, YGNodeRef node) {\n\tstatic const struct set_number setter = {\n\t\tYGNodeStyleSetMinWidth,\n\t\tYGNodeStyleSetMinWidthPercent,\n\t\tNULL,\n\t\tYGNodeStyleSetMinWidthMaxContent,\n\t\tYGNodeStyleSetMinWidthFitContent,\n\t};\n\tsetNumber(L, node, &setter);\n}\n\nstatic void\nlsetMaxWidth(lua_State *L, YGNodeRef node) {\n\tstatic const struct set_number setter = {\n\t\tYGNodeStyleSetMaxWidth,\n\t\tYGNodeStyleSetMaxWidthPercent,\n\t\tNULL,\n\t\tYGNodeStyleSetMaxWidthMaxContent,\n\t\tYGNodeStyleSetMaxWidthFitContent,\n\t};\n\tsetNumber(L, node, &setter);\n}\n\nstatic void\nlsetHeight(lua_State *L, YGNodeRef node) {\n\tstatic const struct set_number setter = {\n\t\tYGNodeStyleSetHeight,\n\t\tYGNodeStyleSetHeightPercent,\n\t\tYGNodeStyleSetHeightAuto,\n\t\tYGNodeStyleSetHeightMaxContent,\n\t\tYGNodeStyleSetHeightFitContent,\n\t};\n\tsetNumber(L, node, &setter);\n}\n\nstatic void\nlsetMinHeight(lua_State *L, YGNodeRef node) {\n\tstatic const struct set_number setter = {\n\t\tYGNodeStyleSetMinHeight,\n\t\tYGNodeStyleSetMinHeightPercent,\n\t\tNULL,\n\t\tYGNodeStyleSetMinHeightMaxContent,\n\t\tYGNodeStyleSetMinHeightFitContent,\n\t};\n\tsetNumber(L, node, &setter);\n}\n\nstatic void\nlsetMaxHeight(lua_State *L, YGNodeRef node) {\n\tstatic const struct set_number setter = {\n\t\tYGNodeStyleSetMaxHeight,\n\t\tYGNodeStyleSetMaxHeightPercent,\n\t\tNULL,\n\t\tYGNodeStyleSetMaxHeightMaxContent,\n\t\tYGNodeStyleSetMaxHeightFitContent,\n\t};\n\tsetNumber(L, node, &setter);\n}\n\nstatic const char *\nskip_whitespace(const char *v) {\n\twhile(*v == ' ' || *v == '\\t') {\n\t\t++v;\n\t}\n\treturn v;\n}\n\nstatic int\ncount_words(const char *v) {\n\tint n = 0;\n\tdo {\n\t\tv = skip_whitespace(v);\n\t\tif (*v != '\\0') {\n\t\t\t++n;\n\t\t\twhile (!is_whitespace(*v))\n\t\t\t\t++v;\n\t\t}\n\t} while (*v != '\\0');\n\treturn n;\n}\n\nstatic const char *\nsetEdgeNumber(lua_State *L, YGNodeRef node, YGEdge edge, const char *v, const struct set_edge_number *setter) {\n\tv = skip_whitespace(v);\n\tchar* endptr = NULL;\n\tfloat number = strtof(v, &endptr);\n\tif (is_whitespace(*endptr)) {\n\t\tsetter->set(node, edge, number);\n\t\treturn endptr;\n\t} else if (setter->setPercent && *endptr == '%') {\n\t\tsetter->setPercent(node, edge, number);\n\t\treturn endptr+1;\n\t} else if (setter->setAuto && memcmp(\"auto\", v, 4) == 0) {\n\t\tif (!is_whitespace(v[4]))\n\t\t\tluaL_error(L, \"Invalid number %s\", v);\n\t\tsetter->setAuto(node, edge);\n\t\treturn v + 4;\n\t} else {\n\t\tluaL_error(L, \"Invalid number %s\", v);\n\t}\n\treturn NULL;\n}\n\nstatic void\nsetFourNumber(lua_State *L, YGNodeRef node, const struct set_edge_number *setter) {\n\tif (lua_type(L, -1) == LUA_TNUMBER) {\n\t\tfloat v = lua_tonumber(L, -1);\n\t\tsetter->set(node, YGEdgeAll, v);\n\t} else {\n\t\tconst char * v = luaL_checkstring(L, -1);\n\t\tswitch (count_words(v)) {\n\t\tcase 1:\n\t\t\tsetEdgeNumber(L, node, YGEdgeAll, v, setter);\n\t\t\tbreak;\n\t\tcase 2:\n\t\t\tv = setEdgeNumber(L, node, YGEdgeVertical, v, setter);\n\t\t\tsetEdgeNumber(L, node, YGEdgeHorizontal, v, setter);\n\t\t\tbreak;\n\t\tcase 3:\n\t\t\tv = setEdgeNumber(L, node, YGEdgeTop, v, setter);\n\t\t\tv = setEdgeNumber(L, node, YGEdgeHorizontal, v, setter);\n\t\t\tsetEdgeNumber(L, node, YGEdgeBottom, v, setter);\n\t\t\tbreak;\n\t\tcase 4:\n\t\t\tv = setEdgeNumber(L, node, YGEdgeTop, v, setter);\n\t\t\tv = setEdgeNumber(L, node, YGEdgeEnd, v, setter);\n\t\t\tv = setEdgeNumber(L, node, YGEdgeBottom, v, setter);\n\t\t\tsetEdgeNumber(L, node, YGEdgeStart, v, setter);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tluaL_error(L, \"Invalid numbers %s\", v);\n\t\t}\n\t}\n}\n\nstatic const char *\nsetGapNumber(lua_State *L, YGNodeRef node, YGGutter gutter, const char *v, const struct set_two_number *setter) {\n\tv = skip_whitespace(v);\n\tchar* endptr = NULL;\n\tfloat number = strtof(v, &endptr);\n\tif (is_whitespace(*endptr)) {\n\t\tsetter->set(node, gutter, number);\n\t\treturn endptr;\n\t} else if (setter->setPercent && *endptr == '%') {\n\t\tsetter->setPercent(node, gutter, number);\n\t\treturn endptr+1;\n\t} else {\n\t\tluaL_error(L, \"Invalid number %s\", v);\n\t}\n\treturn NULL;\n}\n\nstatic void\nsetTwoNumber(lua_State *L, YGNodeRef node, const struct set_two_number *setter) {\n\tif (lua_type(L, -1) == LUA_TNUMBER) {\n\t\tfloat v = lua_tonumber(L, -1);\n\t\tsetter->set(node, YGGutterAll, v);\n\t} else {\n\t\tconst char * v = luaL_checkstring(L, -1);\n\t\tswitch (count_words(v)) {\n\t\tcase 1:\n\t\t\tsetGapNumber(L, node, YGGutterAll, v, setter);\n\t\t\tbreak;\n\t\tcase 2:\n\t\t\tv = setGapNumber(L, node, YGGutterRow, v, setter);\n\t\t\tsetGapNumber(L, node, YGGutterColumn, v, setter);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tluaL_error(L, \"Invalid numbers %s\", v);\n\t\t}\n\t}\n}\n\nstatic const char *\ngetNumber(lua_State *L, const char *v, float *num) {\n\tchar* endptr = NULL;\n\t*num = strtof(v, &endptr);\n\tif (!is_whitespace(*endptr))\n\t\tluaL_error(L, \"Invalid number %s\", v);\n\treturn endptr;\n}\n\nstatic void\nsetFlexBasis(lua_State *L, YGNodeRef node, const char *v) {\n\tstatic const struct set_number setter = {\n\t\tYGNodeStyleSetFlexBasis,\n\t\tYGNodeStyleSetFlexBasisPercent,\n\t\tYGNodeStyleSetFlexBasisAuto,\n\t\tYGNodeStyleSetFlexBasisMaxContent,\n\t\tYGNodeStyleSetFlexBasisFitContent,\n\t};\n\tv = skip_whitespace(v);\n\tsetNumberString(L, node, v, &setter);\n}\n\nstatic void\nlsetFlex(lua_State *L, YGNodeRef node) {\n\tif (lua_type(L, -1) == LUA_TNUMBER) {\n\t\tfloat v = lua_tonumber(L, -1);\n\t\tYGNodeStyleSetFlex(node, v);\n\t} else {\n\t\tconst char * v = luaL_checkstring(L, -1);\n\t\tchar* endptr = NULL;\n\t\tfloat number;\n\t\t\n\t\t// https://developer.mozilla.org/en-US/docs/Web/CSS/flex\n\t\tswitch (count_words(v)) {\n\t\tcase 1:\n\t\t\t// only one word\n\t\t\tnumber = strtof(v, &endptr);\n\t\t\tif (is_whitespace(*endptr)) {\n\t\t\t\t// is number, example : 1\n\t\t\t\t// flex-glow 1 0%\n\t\t\t\tYGNodeStyleSetFlex(node, number);\n\t\t\t} else {\n\t\t\t\t// not a number, example : 50%\n\t\t\t\t// 1 1 flex-basis\n\t\t\t\tYGNodeStyleSetFlexGrow(node, 1);\n\t\t\t\tYGNodeStyleSetFlexShrink(node, 1);\n\t\t\t\tsetFlexBasis(L, node, v);\n\t\t\t}\n\t\t\tbreak;\n\t\tcase 2:\n\t\t\tv = getNumber(L, v, &number);\n\t\t\tYGNodeStyleSetFlexGrow(node, number);\n\t\t\tnumber = strtof(v, &endptr);\n\t\t\tif (is_whitespace(*endptr)) {\n\t\t\t\tYGNodeStyleSetFlexShrink(node, number);\n\t\t\t\tYGNodeStyleSetFlexBasisPercent(node, 0);\n\t\t\t} else {\n\t\t\t\tYGNodeStyleSetFlexShrink(node, 1);\n\t\t\t\tsetFlexBasis(L, node, v);\n\t\t\t}\n\t\t\tbreak;\n\t\tcase 3:\n\t\t\tv = getNumber(L, v, &number);\n\t\t\tYGNodeStyleSetFlexGrow(node, number);\n\t\t\tv = getNumber(L, v, &number);\n\t\t\tYGNodeStyleSetFlexShrink(node, number);\n\t\t\tsetFlexBasis(L, node, v);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tluaL_error(L, \"Invalid flex %s\", v);\n\t\t}\n\t}\n}\n\nstatic void\nlsetMargin(lua_State *L, YGNodeRef node) {\n\tstatic const struct set_edge_number setter = {\n\t\tYGNodeStyleSetMargin,\n\t\tYGNodeStyleSetMarginPercent,\n\t\tYGNodeStyleSetMarginAuto,\n\t};\n\tsetFourNumber(L, node, &setter);\n}\n\nstatic void\nlsetPadding(lua_State *L, YGNodeRef node) {\n\tstatic const struct set_edge_number setter = {\n\t\tYGNodeStyleSetPadding,\n\t\tYGNodeStyleSetPaddingPercent,\n\t\tNULL,\n\t};\n\tsetFourNumber(L, node, &setter);\n}\n\nstatic void\nlsetBorder(lua_State *L, YGNodeRef node) {\n\tstatic const struct set_edge_number setter = {\n\t\tYGNodeStyleSetBorder,\n\t\tNULL,\n\t\tNULL,\n\t};\n\tsetFourNumber(L, node, &setter);\n}\n\nstatic void\nlsetGap(lua_State *L, YGNodeRef node) {\n\tstatic const struct set_two_number setter = {\n\t\tYGNodeStyleSetGap,\n\t\tYGNodeStyleSetGapPercent,\n\t};\n\tsetTwoNumber(L, node, &setter);\n}\n\nstatic int\ngetEnum(lua_State *L, int type, const char *pname) {\n\tlua_pushvalue(L, -1);\n\tif (lua_rawget(L, lua_upvalueindex(2)) == LUA_TNUMBER) {\n\t\tint v = lua_tointeger(L, -1);\n\t\tif (((v >> 16) & type) == type) {\n\t\t\tv &= 0xffff;\n\t\t\treturn v;\n\t\t}\n\t}\n\treturn luaL_error(L, \"Invalid enum %s for %s\", luaL_tolstring(L, -2, NULL), pname);\n}\n\nstatic int\ngetEnumHigh(lua_State *L, int type, const char *pname) {\n\tint e = getEnum(L, type, pname);\n\treturn e >> 8;\n}\n\nstatic int\ngetEnumLow(lua_State *L, int type, const char *pname) {\n\tint e = getEnum(L, type, pname);\n\treturn e & 0xff;\n}\n\nstatic void\nlsetFlexDirection(lua_State *L, YGNodeRef node) {\n\tYGNodeStyleSetFlexDirection(node, getEnum(L, FlexDirection, \"flex-direction\"));\n}\n\nstatic void\nlsetJustifyContent(lua_State *L, YGNodeRef node) {\n\tYGNodeStyleSetJustifyContent(node, getEnumHigh(L, Justify, \"justify-content\"));\n}\n\nstatic void\nlsetAlignItems(lua_State *L, YGNodeRef node) {\n\tYGNodeStyleSetAlignItems(node, getEnumLow(L, Align, \"align-items\"));\n}\n\nstatic void\nlsetAlignContent(lua_State *L, YGNodeRef node) {\n\tYGNodeStyleSetAlignContent(node, getEnumLow(L, Align, \"align-content\"));\n}\n\nstatic void\nlsetAlignSelf(lua_State *L, YGNodeRef node) {\n\tYGNodeStyleSetAlignSelf(node, getEnumLow(L, Align, \"align-self\"));\n}\n\nstatic void\nlsetWrap(lua_State *L, YGNodeRef node) {\n\tYGNodeStyleSetFlexWrap(node, getEnum(L, Wrap, \"wrap\"));\n}\n\nstatic void\nlsetDisplay(lua_State *L, YGNodeRef node) {\n\tYGNodeStyleSetDisplay(node, getEnum(L, Display, \"display\"));\n}\n\nstatic void\nlsetPosition(lua_State *L, YGNodeRef node) {\n\tYGNodeStyleSetPositionType(node, getEnum(L, PositionType, \"position\"));\n}\n\nstatic void\nsetPosition(lua_State *L, YGNodeRef node, YGEdge edge) {\n\tstatic const struct set_edge_number setter = {\n\t\tYGNodeStyleSetPosition,\n\t\tYGNodeStyleSetPositionPercent,\n\t\tYGNodeStyleSetPositionAuto,\n\t};\n\tif (lua_type(L, -1) == LUA_TNUMBER) {\n\t\tfloat v = luaL_checknumber(L, -1);\n\t\tsetter.set(node, edge, v);\n\t} else {\n\t\tconst char *v = luaL_checkstring(L, -1);\n\t\tsetEdgeNumber(L, node, edge, v, &setter);\n\t}\n}\n\n\nstatic void\nlsetTop(lua_State *L, YGNodeRef node) {\n\tsetPosition(L, node, YGEdgeTop);\n}\n\nstatic void\nlsetBottom(lua_State *L, YGNodeRef node) {\n\tsetPosition(L, node, YGEdgeBottom);\n}\n\nstatic void\nlsetLeft(lua_State *L, YGNodeRef node) {\n\tsetPosition(L, node, YGEdgeLeft);\n}\n\nstatic void\nlsetRight(lua_State *L, YGNodeRef node) {\n\tsetPosition(L, node, YGEdgeRight);\n}\n\nstatic void\nlsetAspectRatio(lua_State *L, YGNodeRef node) {\n\tfloat v = luaL_checknumber(L, -1);\n\tYGNodeStyleSetAspectRatio(node, v);\n}\n\nstatic void\nset_array(lua_State *L, YGNodeRef node) {\n\tlua_pushnil(L);\n\tint top = lua_gettop(L);\n\twhile (lua_next(L, 2) != 0) {\n\t\tlua_pushvalue(L, -2);\n\t\tif (lua_rawget(L, lua_upvalueindex(1)) == LUA_TLIGHTUSERDATA) {\n\t\t\tsetfunc func = (setfunc)lua_touserdata(L, -1);\n\t\t\tlua_pop(L, 1);\n\t\t\tfunc(L, node);\n\t\t}\n\t\tlua_settop(L, top);\n\t}\n}\n\nstatic void\nset_one(lua_State *L, YGNodeRef node) {\n\tlua_settop(L, 3);\n\tlua_pushvalue(L, 2);\n\tif (lua_rawget(L, lua_upvalueindex(1)) == LUA_TLIGHTUSERDATA) {\n\t\tsetfunc func = (setfunc)lua_touserdata(L, -1);\n\t\tlua_pop(L, 1);\n\t\tfunc(L, node);\n\t} else {\n\t\tluaL_error(L, \"Invalid attrib name : %s\", lua_tostring(L, 2));\n\t}\n}\n\nstatic int\nlnodeSet(lua_State *L) {\n\tYGNodeRef node = lua_touserdata(L, 1);\n\tswitch(lua_type(L, 2)) {\n\tcase LUA_TTABLE:\n\t\tset_array(L, node);\n\t\tbreak;\n\tcase LUA_TSTRING:\n\t\tset_one(L, node);\n\t\tbreak;\n\tdefault:\n\t\treturn luaL_error(L, \"Set table or key, value\");\n\t}\n\treturn 0;\n}\n\nLUAMOD_API int\nluaopen_layout_yoga(lua_State *L) {\n\tluaL_checkversion(L);\n\tluaL_Reg l[] = {\n\t\t{ \"node_new\", lnodeNew },\n\t\t{ \"node_free\", lnodeFree },\n\t\t{ \"node_calc\", lnodeCalc },\n\t\t{ \"node_get\", lnodeGet },\n\t\t{ \"node_set\", NULL },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\t\n\tstruct {\n\t\tconst char *name;\n\t\tsetfunc func;\n\t} setter [] = {\n\t\t{ \"width\", lsetWidth },\n\t\t{ \"height\", lsetHeight },\n\t\t{ \"minWidth\", lsetMinWidth },\n\t\t{ \"maxWidth\", lsetMaxWidth },\n\t\t{ \"minHeight\", lsetMinHeight },\n\t\t{ \"maxHeight\", lsetMaxHeight },\n\t\t{ \"direction\", lsetFlexDirection },\n\t\t{ \"justify\", lsetJustifyContent },\n\t\t{ \"alignItems\", lsetAlignItems },\n\t\t{ \"alignContent\", lsetAlignContent },\n\t\t{ \"alignSelf\", lsetAlignSelf },\n\t\t{ \"margin\", lsetMargin },\n\t\t{ \"padding\", lsetPadding },\n\t\t{ \"border\", lsetBorder },\n\t\t{ \"gap\", lsetGap },\n\t\t{ \"wrap\", lsetWrap },\n\t\t{ \"display\", lsetDisplay },\n\t\t{ \"flex\", lsetFlex },\n\t\t{ \"position\", lsetPosition },\n\t\t{ \"top\", lsetTop },\n\t\t{ \"bottom\", lsetBottom },\n\t\t{ \"left\", lsetLeft },\n\t\t{ \"right\", lsetRight },\n\t\t{ \"aspectRatio\", lsetAspectRatio },\n\t};\n\tint n = sizeof(setter) / sizeof(setter[0]);\n\tint i;\n\tlua_createtable(L, n, 0);\n\tfor (i=0;i<n;i++) {\n\t\tlua_pushlightuserdata(L, (void *)setter[i].func);\n\t\tlua_setfield(L, -2, setter[i].name);\n\t}\n\n\tstruct enum_string\testr[] = {\n\t\tENUM(FlexDirection, Column)\n\t\tENUM(FlexDirection, ColumnReverse)\n\t\tENUM(FlexDirection, Row)\n\t\tENUM(FlexDirection, RowReverse)\n\t\tENUM(Justify, FlexStart)\n\t\tENUM(Justify, Center)\n\t\tENUM(Justify, FlexEnd)\n\t\tENUM(Justify, SpaceBetween)\n\t\tENUM(Justify, SpaceAround)\n\t\tENUM(Justify, SpaceEvenly)\n\t\tENUM(Align, Auto)\n\t\tENUM(Align, FlexStart)\n\t\tENUM(Align, Center)\n\t\tENUM(Align, FlexEnd)\n\t\tENUM(Align, Baseline)\n\t\tENUM(Align, SpaceBetween)\n\t\tENUM(Align, SpaceAround)\n\t\tENUM(Align, SpaceEvenly)\n\t\tENUM(Wrap, NoWrap)\n\t\tENUM(Wrap, Wrap)\n\t\tENUM(Wrap, WrapReverse)\n\t\tENUM(Display, Flex)\n\t\tENUM(Display, None)\n\t\tENUM(Display, Contents)\n\t\tENUM(PositionType, Static)\n\t\tENUM(PositionType, Relative)\n\t\tENUM(PositionType, Absolute)\n\t};\n\tn = sizeof(estr) / sizeof(estr[0]);\n\tlua_createtable(L, n, 0);\n\tfor (i=0;i<n;i++) {\n\t\tint v = 0;\n\t\tif (lua_getfield(L, -1, estr[i].name) == LUA_TNUMBER) {\n\t\t\tv = lua_tointeger(L, -1);\n\t\t\tv = (v & ~0xffff) | ((v & 0xff) << 8);\n\t\t}\n\t\tlua_pop(L, 1);\n\t\t// align use high 8bits\n\t\tlua_pushinteger(L, estr[i].value | v);\n\t\tlua_setfield(L, -2, estr[i].name);\n\t}\n\tlua_pushcclosure(L, lnodeSet, 2);\n\tlua_setfield(L, -2, \"node_set\");\n\treturn 1;\n}\n"
  },
  {
    "path": "src/luazip.c",
    "content": "#include \"lua.h\"\n#include \"lauxlib.h\"\n#include \"zipreader.h\"\n#include \"zlib/zlib.h\"\n#include \"zlib/contrib/minizip/zip.h\"\n#include \"zlib/contrib/minizip/unzip.h\"\n\n#include <stdlib.h>\n#include <stdio.h>\n#include <stdint.h>\n#include <string.h>\n\n#define ZLIB_UTF8_FLAG (1<<11)\n#define FILECHUNK (4096 * 4)\n\nstatic void *\nexternal_free(void *ud, void *ptr, size_t osize, size_t nsize) {\n\tfree(ptr);\n\treturn NULL;\n}\n\nstatic int\nlcompress(lua_State *L) {\n\tsize_t sz;\n\tconst char * src = luaL_checklstring(L, 1, &sz);\n\tuLongf len = compressBound(sz);\n\tchar * buf = (char *)malloc(len + 1);\n\tif (buf == NULL) {\n\t\treturn luaL_error(L, \"Compress OOM\");\n\t}\n\tif (compress((void *)(buf + 1), &len, (void *)src, sz) != Z_OK) {\n\t\tfree(buf);\n\t\treturn luaL_error(L, \"Compress error\");\n\t}\n\tint idx = 0;\n\tsize_t tmp = len;\n\twhile (sz > tmp) {\n\t\t++idx;\n\t\ttmp *= 2;\n\t}\n\t// 0 : the same size\n\t// 1 : 2x size\n\t// 2...n : (1<<n)x size\n\tbuf[0] = idx;\n\tlua_pushexternalstring(L, buf, len+1, external_free, NULL);\n\treturn 1;\n}\n\nstatic int\nluncompress(lua_State *L) {\n\tsize_t sz;\n\tconst char *src = luaL_checklstring(L, 1, &sz);\n\tint idx = src[0];\n\tuLongf dsz = (1ull << idx) * (sz - 1);\n\tvoid *buf = malloc(dsz);\n\tif (buf == NULL)\n\t\treturn luaL_error(L, \"Uncompress OOM\");\n\tint r = uncompress(buf, &dsz, (void *)(src + 1), sz - 1);\n\tif (r == Z_OK) {\n\t\tlua_pushexternalstring(L, buf, dsz, external_free, NULL);\n\t\treturn 1;\n\t}\n\tfree(buf);\n\tswitch (r) {\n\tcase Z_DATA_ERROR:\n\t\treturn luaL_error(L, \"Uncompress data corrupted\");\n\tcase Z_BUF_ERROR:\n\t\treturn luaL_error(L, \"Uncompress not enough buffer\");\n\tdefault:\n\t\treturn luaL_error(L, \"Uncompress error\");\n\t}\n\treturn 0;\n}\n\n#ifdef _WIN32\n\n#include <windows.h>\n#include \"zlib/contrib/minizip/iowin32.h\"\n\nstruct filename_convert {\n\tWCHAR tmp[4096];\n};\n\nstatic zipFile\nzip_open(lua_State *L, const char *filename, int append) {\n\tstruct filename_convert tmp;\n\tif (MultiByteToWideChar(CP_UTF8, 0, filename, -1, tmp.tmp, sizeof(tmp)) == 0) {\n\t\tif (L == NULL)\n\t\t\treturn NULL;\n\t\tluaL_error(L, \"Can't convert %s to utf16\", filename);\n\t}\n\tzlib_filefunc64_def ffunc;\n\tfill_win32_filefunc64W(&ffunc);\n\treturn zipOpen2_64((const char *)tmp.tmp, append ? APPEND_STATUS_ADDINZIP : 0, NULL, &ffunc);\n}\n\nstatic unzFile\nunzip_open(lua_State *L, const char *filename) {\n\tstruct filename_convert tmp;\n\tif (MultiByteToWideChar(CP_UTF8, 0, filename, -1, tmp.tmp, sizeof(tmp)) == 0) {\n\t\tif (L == NULL)\n\t\t\treturn NULL;\n\t\tluaL_error(L, \"Can't convert %s to utf16\", filename);\n\t}\n\tzlib_filefunc64_def ffunc;\n\tfill_win32_filefunc64W(&ffunc);\n\treturn unzOpen2_64((const char *)tmp.tmp, &ffunc);\n}\n\nstatic FILE *\nfile_open(lua_State *L, const char *filename, const char *mode, struct filename_convert *tmp) {\n\tif (MultiByteToWideChar(CP_UTF8, 0, filename, -1, tmp->tmp, sizeof(*tmp)) == 0) {\n\t\tif (L == NULL)\n\t\t\treturn NULL;\n\t\tluaL_error(L, \"Can't convert %s to utf16\", filename);\n\t}\n\tWCHAR m[32];\n\tint i;\n\tfor (i=0; mode[i]; i++) {\n\t\tif (i == 31) {\n\t\t\tif (L == NULL)\n\t\t\t\treturn NULL;\n\t\t\tluaL_error(L, \"Invalid mode %s\", mode);\n\t\t}\n\t\tm[i] = mode[i];\n\t}\n\tm[i] = 0;\n\treturn _wfopen(tmp->tmp, m);\n}\n\n#else\n\nstruct filename_convert {};\n\nstatic zipFile\nzip_open(lua_State *L, const char *filename, int append) {\n\treturn zipOpen(filename, append ? APPEND_STATUS_ADDINZIP : 0);\n}\n\nstatic unzFile\nunzip_open(lua_State *L, const char *filename) {\n\treturn unzOpen2(filename, 0);\n}\n\nstatic FILE *\nfile_open(lua_State *L, const char *filename, const char *mode, struct filename_convert *tmp) {\n\treturn fopen(filename, mode);\n}\n\n#endif\n\nstruct ziphandle {\n\tzipFile h;\n};\n\nstruct zipraw {\n\tint method;\n\tint level;\n};\n\nstatic zipFile\nopen_new(lua_State *L, int index, const struct zipraw *raw, int level) {\n\tconst char *filename = luaL_checkstring(L, index);\n\tstruct ziphandle *z = (struct ziphandle *)luaL_checkudata(L, 1, \"ZIP_WRITE\");\n\tif (z->h == NULL)\n\t\tluaL_error(L, \"Error: closed\");\n\tif (lua_getiuservalue(L, 1, 1) != LUA_TTABLE)\n\t\tluaL_error(L, \"Invalid zip userdata\");\n\tint cache = lua_gettop(L);\n\tlua_pushvalue(L, index);\n\tif (lua_rawget(L, cache) != LUA_TNIL) {\n\t\tluaL_error(L, \"Error: %s exist\", filename);\n\t}\n\tint err = zipOpenNewFileInZip4(z->h, filename, NULL, NULL, 0, NULL, 0, NULL,\n\t\traw ? raw->method : Z_DEFLATED,\n\t\traw ? raw->level : level,\n\t\traw != NULL,\n\t\t-MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,\n\t\tNULL, 0, 0,\n\t\tZLIB_UTF8_FLAG\n\t);\n\tif (err != ZIP_OK) {\n\t\tluaL_error(L, \"Error: open in file\");\n\t}\n\tlua_pushvalue(L, 2);\n\tlua_pushboolean(L, 1);\n\tlua_rawset(L, cache);\n\tlua_settop(L, cache - 1);\n\treturn z->h;\n}\n\nstatic inline void\nclose_inzip(lua_State *L, zipFile zf) {\n\tif (zipCloseFileInZip(zf) != ZIP_OK) {\n\t\tluaL_error(L, \"Error: close in file\");\n\t}\n}\n\nstatic int\nzipwrite_add(lua_State *L) {\n\tint level = luaL_optinteger(L, 4, Z_DEFAULT_COMPRESSION);\n\tzipFile zf = open_new(L, 2, NULL, level);\n\tsize_t sz;\n\tconst char * content = luaL_checklstring(L, 3, &sz);\n\tint err = zipWriteInFileInZip(zf, content, sz);\n\tif (err != ZIP_OK) {\n\t\treturn luaL_error(L, \"Error: write in file\");\n\t}\n\tclose_inzip(L, zf);\n\treturn 0;\n}\n\nstatic int\nzipwrite_addfile(lua_State *L) {\n\tint level = luaL_optinteger(L, 4, Z_DEFAULT_COMPRESSION);\n\tzipFile zf = open_new(L, 2, NULL, level);\n\tconst char * addfile = luaL_checkstring(L, 3);\n\tstruct filename_convert tmp;\n\tFILE *f = file_open(L, addfile, \"rb\", &tmp);\n\tif (f == NULL)\n\t\treturn luaL_error(L, \"Can't open %s\", addfile);\n\tchar buf[FILECHUNK];\n\tfor (;;) {\n\t\tint bytes = fread(buf, 1, FILECHUNK, f);\n\t\tif (bytes <= 0) {\n\t\t\tif (bytes == 0)\n\t\t\t\tbreak;\n\t\t\treturn luaL_error(L, \"Error: read file %s\", addfile);\n\t\t}\n\t\tint err = zipWriteInFileInZip(zf, buf, bytes);\n\t\tif (err != ZIP_OK) {\n\t\t\tfclose(f);\n\t\t\treturn luaL_error(L, \"Error: write in file\");\n\t\t}\n\t\tif (bytes < FILECHUNK)\n\t\t\tbreak;\n\t}\n\tfclose(f);\n\tclose_inzip(L, zf);\n\treturn 0;\n}\n\nstatic int\nzipwrite_open(lua_State *L) {\n\tint level = luaL_optinteger(L, 3, Z_DEFAULT_COMPRESSION);\n\topen_new(L, 2, NULL, level);\n\treturn 0;\n}\n\nstatic int\nzipwrite_close(lua_State *L) {\n\tstruct ziphandle *z = (struct ziphandle *)luaL_checkudata(L, 1, \"ZIP_WRITE\");\n\tif (z->h == NULL)\n\t\tluaL_error(L, \"Error: closed\");\n\tclose_inzip(L, z->h);\n\treturn 0;\n}\n\nstatic int\nzipwrite_closezip(lua_State *L) {\n\tstruct ziphandle *z = (struct ziphandle *)luaL_checkudata(L, 1, \"ZIP_WRITE\");\n\tif (z->h == NULL)\n\t\treturn 0;\n\tint err = zipClose(z->h, NULL);\n\tz->h = NULL;\n\tif (err != Z_OK)\n\t\treturn luaL_error(L, \"Error: close\");\n\treturn 0;\n}\n\nstatic int\nzipwrite_write(lua_State *L) {\n\tstruct ziphandle *z = (struct ziphandle *)luaL_checkudata(L, 1, \"ZIP_WRITE\");\n\tif (z->h == NULL)\n\t\tluaL_error(L, \"Error: closed\");\n\tsize_t sz;\n\tconst char *content = luaL_checklstring(L, 2, &sz);\n\tint err = zipWriteInFileInZip(z->h, content, sz);\n\tif (err != ZIP_OK) {\n\t\treturn luaL_error(L, \"Error: write in file\");\n\t}\n\treturn 0;\n}\n\nstruct unzhandle {\n\tunzFile h;\n};\n\nstatic int\nzipread_closezip(lua_State *L) {\n\tstruct unzhandle *z = (struct unzhandle *)luaL_checkudata(L, 1, \"ZIP_READ\");\n\tif (z->h == NULL)\n\t\tluaL_error(L, \"Error: closed\");\n\tint err = unzClose(z->h);\n\tz->h = NULL;\n\tif (err != UNZ_OK)\n\t\treturn luaL_error(L, \"Error: close\");\n\treturn 0;\n}\n\nstatic inline lua_Integer\nfile_pos_to_luaint(const unz_file_pos *pos) {\n\tuint64_t p = pos->pos_in_zip_directory;\n\tuint64_t n = pos->num_of_file;\n\treturn (lua_Integer)(p << 32 | n);\n}\n\nstatic inline unz_file_pos *\nluaint_to_file_pos(lua_Integer v, unz_file_pos *pos) {\n\tpos->pos_in_zip_directory = (uint64_t)v >> 32;\n\tpos->num_of_file = v & 0xffffffff;\n\treturn pos;\n}\n\nstatic void\nget_filelist(lua_State *L, unzFile zf) {\n\tlua_newtable(L);\n\tint err = unzGoToFirstFile(zf);\n\tif (err != UNZ_OK)\n\t\tluaL_error(L, \"Error: goto first file\");\n\tchar filename[4096];\n\tfor (;;) {\n\t\tunz_file_pos pos;\n\t\tunzGetFilePos(zf, &pos);\n\t\tint err = unzGetCurrentFileInfo(zf, NULL, filename, sizeof(filename), NULL, 0, NULL, 0);\n\t\tif (err != UNZ_OK)\n\t\t\tluaL_error(L, \"Error: get file info %d\", pos.num_of_file);\n\t\tlua_pushinteger(L, file_pos_to_luaint(&pos));\n\t\tlua_setfield(L, -2, filename);\n\t\terr = unzGoToNextFile(zf);\n\t\tif (err != UNZ_OK) {\n\t\t\tif (err == UNZ_END_OF_LIST_OF_FILE)\n\t\t\t\tbreak;\n\t\t\tluaL_error(L, \"Error: goto next file %d\", pos.num_of_file);\n\t\t}\n\t}\n}\n\nstatic int\nzipread_list(lua_State *L) {\n\tif (lua_getiuservalue(L, 1, 1) != LUA_TTABLE)\n\t\treturn luaL_error(L, \"Invalid zip userdata\");\n\tint t = lua_gettop(L);\n\tlua_newtable(L);\n\tint r = t+1;\n\tlua_pushnil(L);\n\twhile (lua_next(L, t) != 0) {\n\t\tif (lua_type(L, -1) == LUA_TNUMBER) {\n\t\t\tint n = luaL_checkinteger(L, -1);\n\t\t\tlua_pop(L, 1);\n\t\t\tlua_pushvalue(L, -1);\n\t\t\tlua_rawseti(L, r, n+1);\n\t\t} else {\n\t\t\t// filename\n\t\t\tlua_pop(L, 1);\n\t\t}\n\t}\n\treturn 1;\n}\n\nstatic void\nlocate_file(lua_State *L, unzFile zf, lua_Integer pos) {\n\tunz_file_pos tmp;\n\tint err = unzGoToFilePos(zf, luaint_to_file_pos(pos, &tmp));\n\tif (err != UNZ_OK)\n\t\tluaL_error(L, \"Error: unzGoToFilePos\");\n}\n\nstatic int\nzipread_exist(lua_State *L) {\n\tif (lua_getiuservalue(L, 1, 1) != LUA_TTABLE) {\n\t\tluaL_error(L, \"Invalid zip userdata\");\n\t}\n\tlua_pushvalue(L, 2);\t// filename\n\tint exist = (lua_rawget(L, -2) == LUA_TNUMBER);\n\tlua_pushboolean(L, exist);\n\treturn 1;\n}\n\nstatic unzFile\nopen_file(lua_State *L, int rzip, int filename, struct zipraw *raw) {\n\tif (lua_getiuservalue(L, rzip, 1) != LUA_TTABLE) {\n\t\tluaL_error(L, \"Invalid zip userdata\");\n\t}\n\tlua_pushvalue(L, filename);\t// filename\n\tif (lua_rawget(L, -2) != LUA_TNUMBER) {\n\t\tlua_pop(L, 1);\n\t\treturn NULL;\n\t}\n\tlua_Integer pos = luaL_checkinteger(L, -1);\n\tlua_pop(L, 2);\n\n\tstruct unzhandle *z = (struct unzhandle *)luaL_checkudata(L, rzip, \"ZIP_READ\");\n\tif (z->h == NULL)\n\t\tluaL_error(L, \"Error: closed\");\n\n\tlocate_file(L, z->h, pos);\n\tint err;\n\tif (raw) {\n\t\terr = unzOpenCurrentFile2(z->h, &raw->method, &raw->level, 1);\n\t} else {\n\t\terr = unzOpenCurrentFile(z->h);\n\t}\n\tif (err != UNZ_OK)\n\t\tluaL_error(L, \"Error: open file %s\", lua_tostring(L, filename));\n\treturn z->h;\n}\n\nstatic void\nclose_file(lua_State *L, unzFile zf) {\n\tint err = unzCloseCurrentFile(zf);\n\tif (err != UNZ_OK) {\n\t\tif (err == UNZ_CRCERROR)\n\t\t\tluaL_error(L, \"Error: CRC\");\n\t\telse\n\t\t\tluaL_error(L, \"Error: close file\");\n\t}\n}\n\nstatic int\nzipread_readfile(lua_State *L) {\n\tunzFile zf = open_file(L, 1, 2, NULL);\n\tif (zf == NULL)\n\t\treturn 0;\n\tunz_file_info info;\n\tint err = unzGetCurrentFileInfo(zf, &info, NULL, 0, NULL, 0, NULL, 0);\n\tif (err != UNZ_OK) {\n\t\tclose_file(L, zf);\n\t\tluaL_error(L, \"Error: get file info %s\", lua_tostring(L, 2));\n\t}\n\tvoid *buf = malloc(info.uncompressed_size);\n\tif (buf == NULL) {\n\t\tclose_file(L, zf);\n\t\tluaL_error(L, \"Error: out of memory\");\n\t}\n\tint bytes = unzReadCurrentFile(zf, buf, info.uncompressed_size);\n\tif (bytes != info.uncompressed_size) {\n\t\tfree(buf);\n\t\tclose_file(L, zf);\n\t\tluaL_error(L, \"Error: read file %s\", lua_tostring(L, 2));\n\t}\n\t\n\tlua_pushexternalstring(L, buf, bytes, external_free, NULL);\n\tclose_file(L, zf);\n\treturn 1;\n}\n\nstatic int\nzipread_size(lua_State *L) {\n\tunzFile zf = open_file(L, 1, 2, NULL);\n\tif (zf == NULL)\n\t\treturn 0;\n\tunz_file_info info;\n\tint err = unzGetCurrentFileInfo(zf, &info, NULL, 0, NULL, 0, NULL, 0);\n\tif (err != UNZ_OK)\n\t\tluaL_error(L, \"Error: get file info %s\", lua_tostring(L, 2));\n\tlua_pushinteger(L, info.uncompressed_size);\n\treturn 1;\n}\n\nstatic int\nzipread_extract(lua_State *L) {\n\tunzFile zf = open_file(L, 1, 2, NULL);\n\tif (zf == NULL)\n\t\treturn luaL_error(L, \"Error: no file %s\", lua_tostring(L, 2));\n\tconst char * filename = luaL_checkstring(L, 3);\n\tstruct filename_convert tmp;\n\tFILE *f = file_open(L, filename, \"wb\", &tmp);\n\tif (f == NULL) {\n\t\tclose_file(L, zf);\n\t\treturn luaL_error(L, \"Error: open %s\", filename);\n\t}\n\tchar buf[FILECHUNK];\n\tint bytes = 0;\n\tdo {\n\t\tbytes = unzReadCurrentFile(zf, buf, sizeof(buf));\n\t\tif (bytes < 0) {\n\t\t\tclose_file(L, zf);\n\t\t\treturn luaL_error(L, \"Error: read %s\", lua_tostring(L, 2));\n\t\t}\n\t\tif (bytes > 0 && fwrite(buf, 1, bytes, f) != bytes) {\n\t\t\tclose_file(L, zf);\n\t\t\treturn luaL_error(L, \"Error: write %s\", filename);\n\t\t}\n\t} while (bytes == sizeof(buf));\n\tfclose(f);\n\tclose_file(L, zf);\n\treturn 0;\n}\n\nstatic int\nzipread_openfile(lua_State *L) {\n\topen_file(L, 1, 2, NULL);\n\treturn 0;\n}\n\nstatic int\nzipread_closefile(lua_State *L) {\n\tstruct unzhandle *z = (struct unzhandle *)luaL_checkudata(L, 1, \"ZIP_READ\");\n\tif (z->h == NULL)\n\t\tluaL_error(L, \"Error: closed\");\n\tclose_file(L, z->h);\n\treturn 0;\t\n}\n\nstatic int\nzipread_read(lua_State *L) {\n\tstruct unzhandle *z = (struct unzhandle *)luaL_checkudata(L, 1, \"ZIP_READ\");\n\tif (z->h == NULL)\n\t\tluaL_error(L, \"Error: closed\");\n\tint n = luaL_checkinteger(L, 2);\n\tvoid *buf = malloc(n);\n\tif (buf == NULL)\n\t\treturn luaL_error(L, \"Error: out of memory\");\n\tint bytes = unzReadCurrentFile(z->h, buf, n);\n\tif (bytes <= 0) {\n\t\tfree(buf);\n\t\tif (bytes == 0)\n\t\t\treturn 0;\n\t\tluaL_error(L, \"Error: read file\");\n\t}\n\tlua_pushexternalstring(L, buf, bytes, external_free, NULL);\n\treturn 1;\n}\n\n// 2: filename\n// 3: readzip (userdata)\n// 4: opt: altername\nstatic int\nzipwrite_copyfrom(lua_State *L) {\n\tint filename = lua_isnoneornil(L, 4) ? 2 : 4;\n\tstruct zipraw raw;\n\tunzFile rd = open_file(L, 3, filename, &raw);\n\tif (rd == NULL)\n\t\treturn luaL_error(L, \"Error: open %s\", lua_tostring(L, filename));\n\tzipFile zf = open_new(L, 2, &raw, raw.level);\n\tif (zf == NULL) {\n\t\tclose_file(L, rd);\n\t\treturn luaL_error(L, \"Error: open %s\", lua_tostring(L, 2));\n\t}\n\n\t// copy file from rd to zf\n\t// todo : zipCloseFileInZipRaw on error\n\n\tunz_file_info info;\n\tint err = unzGetCurrentFileInfo(rd, &info, NULL, 0, NULL, 0, NULL, 0);\n\tif (err != UNZ_OK) {\n\t\tclose_file(L, rd);\n\t\tluaL_error(L, \"Error: get file info %s\", lua_tostring(L, filename));\n\t}\n\n\tchar buf[FILECHUNK];\n\tint bytes;\n\tdo {\n\t\tbytes = unzReadCurrentFile(rd, buf, sizeof(buf));\n\t\tif (bytes < 0) {\n\t\t\tclose_file(L, rd);\n\t\t\treturn luaL_error(L, \"Error: read %s\", lua_tostring(L, filename));\n\t\t}\n\t\tif (bytes > 0 && zipWriteInFileInZip(zf, buf, bytes) != ZIP_OK) {\n\t\t\tclose_file(L, rd);\n\t\t\treturn luaL_error(L, \"Error: write %s\", lua_tostring(L, 2));\n\t\t}\n\t} while (bytes == sizeof(buf));\n\tclose_file(L, rd);\n\n\tif (zipCloseFileInZipRaw(zf, info.uncompressed_size, info.crc) != ZIP_OK)\n\t\treturn luaL_error(L, \"Error: close %s\", 2);\n\n\treturn 0;\n}\n\nstatic int\nzipread_filename(lua_State *L) {\n\tconst int rzip = 1;\n\tconst int filename = 2;\n\tif (lua_getiuservalue(L, rzip, 1) != LUA_TTABLE) {\n\t\tluaL_error(L, \"Invalid zip userdata\");\n\t}\n\tlua_pushvalue(L, filename);\t// filename\n\tif (lua_rawget(L, -2) != LUA_TNUMBER) {\n\t\treturn 0;\n\t}\n\tlua_Integer v = luaL_checkinteger(L, -1);\n\tlua_pop(L, 1);\n\tunz_file_pos pos;\n\tluaint_to_file_pos(v, &pos);\n\tif (lua_rawgeti(L, -1, 0) != LUA_TSTRING) {\n\t\treturn luaL_error(L, \"No zip filename\");\n\t}\n\tlua_pushfstring(L, \"%s|%d\", lua_tostring(L, -1), pos.num_of_file);\n\treturn 1;\n}\n\nstatic int\nunzip(lua_State *L, const char *filename) {\n\tunzFile zf = unzip_open(L, filename);\n\tif (zf == NULL)\n\t\treturn 0;\n\tstruct unzhandle *z = (struct unzhandle *)lua_newuserdatauv(L, sizeof(*z), 1);\n\tget_filelist(L, zf);\n\tlua_pushstring(L, filename);\n\tlua_rawseti(L, -2, 0);\n\tlua_setiuservalue(L, -2, 1);\n\tz->h = zf;\n\tif (luaL_newmetatable(L, \"ZIP_READ\")) {\n\t\tluaL_Reg l[] = {\n\t\t\t{ \"__index\", NULL },\n\t\t\t{ \"__gc\", zipread_closezip },\n\t\t\t{ \"close\", zipread_closezip },\n\t\t\t{ \"list\", zipread_list },\n\t\t\t{ \"extract\", zipread_extract },\n\t\t\t{ \"readfile\", zipread_readfile },\n\t\t\t{ \"exist\", zipread_exist },\n\t\t\t{ \"openfile\", zipread_openfile },\n\t\t\t{ \"closefile\", zipread_closefile },\n\t\t\t{ \"read\", zipread_read },\n\t\t\t{ \"size\", zipread_size },\n\t\t\t{ \"filename\", zipread_filename },\n\t\t\t{ NULL, NULL },\n\t\t};\n\t\tluaL_setfuncs(L, l, 0);\n\t\tlua_pushvalue(L, -1);\n\t\tlua_setfield(L, -2, \"__index\");\n\t}\n\tlua_setmetatable(L, -2);\n\treturn 1;\n}\n\nstatic int\nzip(lua_State *L, const char *filename, int append) {\n\tif (append) {\n\t\tunzFile uzf = unzip_open(L, filename);\n\t\tif (uzf == NULL)\n\t\t\treturn 0;\n\t\tget_filelist(L, uzf);\n\t\tunzClose(uzf);\n\t} else {\n\t\tlua_newtable(L);\t// cache filenames\n\t}\n\tzipFile zf = zip_open(L, filename, append);\n\tif (zf == NULL)\n\t\treturn 0;\n\tstruct ziphandle *z = (struct ziphandle *)lua_newuserdatauv(L, sizeof(*z), 1);\n\tlua_insert(L, -2);\n\tlua_setiuservalue(L, -2, 1);\n\tz->h = zf;\n\tif (luaL_newmetatable(L, \"ZIP_WRITE\")) {\n\t\tluaL_Reg l[] = {\n\t\t\t{ \"__index\", NULL },\n\t\t\t{ \"__gc\", zipwrite_closezip },\n\t\t\t{ \"copyfrom\", zipwrite_copyfrom },\n\t\t\t{ \"addfile\", zipwrite_addfile },\n\t\t\t{ \"add\", zipwrite_add },\n\t\t\t{ \"openfile\", zipwrite_open },\n\t\t\t{ \"closefile\", zipwrite_close },\n\t\t\t{ \"write\", zipwrite_write },\n\t\t\t{ \"close\", zipwrite_closezip },\n\t\t\t{ NULL, NULL },\n\t\t};\n\t\tluaL_setfuncs(L, l, 0);\n\t\tlua_pushvalue(L, -1);\n\t\tlua_setfield(L, -2, \"__index\");\n\t}\n\tlua_setmetatable(L, -2);\n\treturn 1;\n}\n\nstatic int\nlzip(lua_State *L) {\n\tconst char * filename = luaL_checkstring(L, 1);\n\tconst char * mode = luaL_checkstring(L, 2);\n\tswitch (mode[0]) {\n\tcase 'w':\n\t\treturn zip(L, filename, 0);\n\tcase 'r':\n\t\treturn unzip(L, filename);\n\tcase 'a':\n\t\treturn zip(L, filename, 1);\n\tdefault:\n\t\treturn luaL_error(L, \"Invalid Mode %s\", mode);\n\t}\n}\n\nstatic int\nlziplist(lua_State *L) {\n\tif (lua_isnoneornil(L, 1)) {\n\t\treturn 0;\n\t}\n\tluaL_checktype(L, 1, LUA_TTABLE);\n\tint n = lua_rawlen(L, 1);\n\tstruct zipreader_name * names = (struct zipreader_name *)lua_newuserdatauv(L, sizeof(struct zipreader_name) * (n + 1), n * 2);\n\tint ud_index = lua_gettop(L);\n\tnames[n].zipfile = NULL;\n\tnames[n].root = NULL;\n\tnames[n].root_size = 0;\n\tint i;\n\tfor (i = 0; i < n; i++) {\n\t\tstruct zipreader_name * name = &names[i];\n\t\tif (lua_geti(L, 1, n - i) != LUA_TTABLE) {\n\t\t\treturn luaL_error(L, \"Invalid ziplist table\");\n\t\t}\n\t\tif (lua_getfield(L, -1, \"name\") != LUA_TSTRING) {\n\t\t\treturn luaL_error(L, \"Invalid ziplist table, missing .name\");\n\t\t}\n\t\tname->zipfile = lua_tostring(L, -1);\n\t\tlua_setiuservalue(L, ud_index, i * 2 + 1);\n\t\tif (lua_getfield(L, -1, \"root\") == LUA_TSTRING) {\n\t\t\tname->root = lua_tolstring(L, -1, &name->root_size);\n\t\t\tlua_setiuservalue(L, ud_index, i * 2 + 2);\n\t\t} else if (lua_isnil(L, -1)) {\n\t\t\tname->root = NULL;\n\t\t\tname->root_size = 0;\n\t\t\tlua_pop(L, 1);\n\t\t} else {\n\t\t\treturn luaL_error(L, \"Invalid ziplist table, .root is not a string\");\n\t\t}\n\t\tlua_pop(L, 1);\n\t}\n\treturn 1;\n}\n\n// #define ZIPTEST\n#ifdef ZIPTEST\n\nstatic int lziptest(lua_State *L);\n\n#endif\n\nint\nluaopen_zip(lua_State *L) {\n\tluaL_checkversion(L);\n\tluaL_Reg l[] = {\n\t\t{ \"compress\", lcompress },\n\t\t{ \"uncompress\", luncompress },\n\t\t{ \"open\", lzip },\n\t\t{ \"list\", lziplist },\n#ifdef ZIPTEST\t\t\n\t\t{ \"test\", lziptest },\n#endif\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\treturn 1;\n}\n\n// zip reader\n\nstatic unzFile\ntry_open(struct zipreader_name *name, const char *filename) {\n\tif (name->root) {\n\t\tif (memcmp(name->root, filename, name->root_size) != 0) {\n\t\t\treturn NULL;\n\t\t}\n\t\tfilename += name->root_size;\n\t}\n\tunzFile zf = unzip_open(NULL, name->zipfile);\n\tif (zf == NULL)\n\t\treturn NULL;\n\t\n\tif (unzLocateFile(zf, filename, 0) != UNZ_OK || unzOpenCurrentFile(zf) != UNZ_OK) {\n\t\tunzClose(zf);\n\t\treturn NULL;\n\t}\n\treturn zf;\n}\n\nzipreader_file\nzipreader_open(struct zipreader_name *names, const char * filename) {\n\tint i;\n\tfor (i=0;names[i].zipfile;i++) {\n\t\tunzFile zf = try_open(&names[i], filename);\n\t\tif (zf)\n\t\t\treturn (zipreader_file)zf;\n\t}\n\treturn NULL;\n}\n\nvoid\nzipreader_close(zipreader_file zf) {\n\tunzClose((unzFile)zf);\n}\n\nint\nzipreader_read(zipreader_file zf, void *dst, int bytes) {\n\treturn unzReadCurrentFile(zf, dst, bytes);\n}\n\nint64_t\nzipreader_tell(zipreader_file zf) {\n\treturn unztell64(zf);\n}\n\nsize_t\nzipreader_size(zipreader_file zf) {\n\tunz_file_info64 info;\n\tint err = unzGetCurrentFileInfo64(zf, &info, NULL, 0, NULL, 0, NULL, 0);\n\tif (err != UNZ_OK) {\n\t\treturn 0;\n\t}\n\treturn info.uncompressed_size;\n}\n\nstatic void\nreopen_file(zipreader_file zf) {\n\tunz64_file_pos pos;\n\tunzGetFilePos64(zf, &pos);\n\tunzCloseCurrentFile(zf);\n\tunzGoToFilePos64(zf, &pos);\n\tunzOpenCurrentFile(zf);\n}\n\n#define TMP_SKIP_BUFFER 4096\n\nstatic int\nskip_bytes(zipreader_file zf, int64_t offset) {\n\tchar tmp[TMP_SKIP_BUFFER];\n\twhile (offset >= TMP_SKIP_BUFFER) {\n\t\tif (unzReadCurrentFile(zf, tmp, TMP_SKIP_BUFFER) != TMP_SKIP_BUFFER)\n\t\t\treturn -1;\n\t\toffset -= TMP_SKIP_BUFFER;\n\t}\n\tif (offset > 0 && unzReadCurrentFile(zf, tmp, offset) != offset)\n\t\treturn -1;\n\treturn 0;\n}\n\nint\nzipreader_seek(zipreader_file zf, int64_t offset, int origin) {\n\tif (origin == SEEK_CUR && offset >= 0) {\n\t\treturn skip_bytes(zf, offset);\n\t}\n\tsize_t size = zipreader_size(zf);\n\tsize_t cur_pos = unztell64(zf);\n\tint64_t new_pos;\n\tswitch (origin) {\n\tcase SEEK_SET:\n\t\tnew_pos = offset;\n\t\tbreak;\n\tcase SEEK_CUR:\n\t\tnew_pos = (int64_t)cur_pos + offset;\n\t\tbreak;\n\tcase SEEK_END:\n\t\tnew_pos = (int64_t)size + offset;\n\t\tbreak;\n\tdefault :\n\t\treturn -1;\n\t}\n\tif (new_pos < 0)\n\t\tnew_pos = 0;\n\telse if (new_pos > size)\n\t\tnew_pos = size;\n\tif (new_pos >= cur_pos) {\n\t\treturn skip_bytes(zf, new_pos - cur_pos);\n\t}\n\treopen_file(zf);\n\treturn skip_bytes(zf, new_pos);\n}\n\n#ifdef ZIPTEST\n\nstatic int\nlziptest(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TUSERDATA);\n\tstruct zipreader_name *names = (struct zipreader_name *)lua_touserdata(L, 1);\n\tconst char *filename = luaL_checkstring(L, 2);\n\tzipreader_file zf = zipreader_open(names, filename);\n\tif (zf == NULL)\n\t\treturn 0;\n\tint sz = zipreader_size(zf);\n\tvoid * buf = lua_newuserdatauv(L, sz, 0);\n\tint rd = zipreader_read(zf, buf, sz);\n\t\n\tif (sz != rd) {\n\t\tzipreader_close(zf);\n\t\treturn luaL_error(L, \"Read zipfile error\");\n\t}\n\tlua_pushlstring(L, (const char *)buf, rd);\n\t\n\tzipreader_seek(zf, 10, SEEK_SET);\n\trd = zipreader_read(zf, buf, sz);\n\tlua_pushlstring(L, (const char *)buf, rd);\n\t\n\tzipreader_close(zf);\n\t\n\treturn 2;\n}\n\n#endif\n\n"
  },
  {
    "path": "src/maskquad.glsl",
    "content": "@vs vs\nlayout(binding=0) uniform vs_params {\n\tvec2 framesize;\n\tfloat texsize;\n};\n\nstruct sr_mat {\n\tmat2 m;\n};\n\nlayout(binding=0) readonly buffer sr_lut {\n\tsr_mat sr[];\n};\n\nin vec3 position;\nin vec4 color;\nin uint offset;\nin uint u;\nin uint v;\n\nout vec2 uv;\nout vec4 maskcolor;\n\nvoid main() {\n\tivec2 uv_base = ivec2(u >> 16, v >> 16);\n\tivec2 u2 = ivec2(0 , u & 0xffff);\n\tivec2 v2 = ivec2(0 , v & 0xffff);\n\tivec2 off = ivec2(offset >> 16 , offset & 0xffff) - 0x8000;\n\tvec2 uv_offset = vec2(u2[gl_VertexIndex & 1] , v2[gl_VertexIndex >> 1]);\n\tvec2 pos = ((uv_offset - off) * sr[int(position.z)].m + position.xy) * framesize;\n\tgl_Position = vec4(pos.x - 1.0f, pos.y + 1.0f, 0, 1);\n\tuv = (uv_base + uv_offset) * texsize;\n\tmaskcolor = color;\n}\n\n@end\n\n@fs fs\nlayout(binding=1) uniform texture2D tex;\nlayout(binding=0) uniform sampler smp;\n\nin vec2 uv;\nin vec4 maskcolor;\nout vec4 frag_color;\n\nvoid main() {\n\tfloat alpha = texture(sampler2D(tex,smp), uv).a; \n\tfrag_color = maskcolor;\n\tfrag_color.a = alpha * maskcolor.a;\n}\n@end\n\n@program maskquad vs fs"
  },
  {
    "path": "src/material/matdefault.lua",
    "content": "local render = require \"soluna.render\"\nlocal defmat = require \"soluna.material.default\"\n\nlocal ctx = ...\nlocal state = ctx.state\nlocal setting = ctx.settings\nlocal inst_buffer = render.buffer {\n\ttype = \"vertex\",\n\tusage = \"stream\",\n\tlabel = \"texquad-instance\",\n\tsize = defmat.instance_size * setting.draw_instance,\n}\nlocal bindings = render.bindings()\nbindings:vbuffer(0, inst_buffer)\nbindings:view(0, state.views.storage)\nbindings:sampler(0, state.default_sampler)\n\nstate.inst = assert(inst_buffer)\nstate.bindings = bindings\nstate.material = defmat.new {\n\tinst_buffer = state.inst,\n\tbindings = state.bindings,\n\tuniform = state.uniform,\n\tsr_buffer = state.srbuffer_mem,\n\tsprite_bank = ctx.arg.bank_ptr,\n\ttmp_buffer = ctx.tmp_buffer,\n}\n\nlocal material = {}\n\nfunction material.reset()\n\tbindings:base(0)\nend\n\nfunction material.submit(ptr, n)\n\tstate.material:submit(ptr, n)\nend\n\nfunction material.draw(ptr, n, tex)\n\tbindings:view(1, state.views[tex + 1])\n\tstate.material:draw(ptr, n, tex)\nend\n\nreturn material\n"
  },
  {
    "path": "src/material/matmask.lua",
    "content": "local render = require \"soluna.render\"\nlocal maskmat = require \"soluna.material.mask\"\n\nlocal ctx = ...\nlocal state = ctx.state\nmaskmat.set_material_id(ctx.id)\n\nstate.mask_inst = render.buffer {\n\ttype = \"vertex\",\n\tusage = \"stream\",\n\tlabel = \"mask-instance\",\n\tsize = maskmat.instance_size * ctx.settings.draw_instance,\n}\n\nlocal mask_bindings = render.bindings()\nmask_bindings:vbuffer(0, state.mask_inst)\nmask_bindings:view(0, state.views.storage)\nmask_bindings:sampler(0, state.default_sampler)\n\nstate.mask_bindings = mask_bindings\nstate.material_mask = maskmat.new {\n\tinst_buffer = state.mask_inst,\n\tbindings = state.mask_bindings,\n\tuniform = state.uniform,\n\tsr_buffer = state.srbuffer_mem,\n\tsprite_bank = ctx.arg.bank_ptr,\n\ttmp_buffer = ctx.tmp_buffer,\n}\n\nlocal material = {}\n\nfunction material.reset()\n\tmask_bindings:base(0)\nend\n\nfunction material.submit(ptr, n)\n\tstate.material_mask:submit(ptr, n)\nend\n\nfunction material.draw(ptr, n, tex)\n\tmask_bindings:view(1, state.views[tex + 1])\n\tstate.material_mask:draw(ptr, n, tex)\nend\n\nreturn material\n"
  },
  {
    "path": "src/material/matquad.lua",
    "content": "local render = require \"soluna.render\"\nlocal quadmat = require \"soluna.material.quad\"\n\nlocal ctx = ...\nlocal state = ctx.state\nquadmat.set_material_id(ctx.id)\n\nstate.quad_inst = render.buffer {\n\ttype = \"vertex\",\n\tusage = \"stream\",\n\tlabel = \"quad-instance\",\n\tsize = quadmat.instance_size * ctx.settings.draw_instance,\n}\n\nlocal quad_bindings = render.bindings()\nquad_bindings:vbuffer(0, state.quad_inst)\nquad_bindings:view(0, state.views.storage)\n\nstate.quad_bindings = quad_bindings\nstate.material_quad = quadmat.new {\n\tinst_buffer = state.quad_inst,\n\tbindings = state.quad_bindings,\n\tuniform = state.uniform,\n\tsr_buffer = state.srbuffer_mem,\n\ttmp_buffer = ctx.tmp_buffer,\n}\n\nlocal material = {}\n\nfunction material.reset()\n\tquad_bindings:base(0)\nend\n\nfunction material.submit(ptr, n)\n\tstate.material_quad:submit(ptr, n)\nend\n\nfunction material.draw(ptr, n)\n\tstate.material_quad:draw(ptr, n)\nend\n\nreturn material\n"
  },
  {
    "path": "src/material/mattext.lua",
    "content": "local render = require \"soluna.render\"\nlocal textmat = require \"soluna.material.text\"\n\nlocal ctx = ...\nlocal state = ctx.state\nlocal setting = ctx.settings\ntextmat.set_material_id(ctx.id)\n\nlocal text_bindings\nlocal text_sampler_desc = setting.text_sampler\nif text_sampler_desc then\n\ttext_sampler_desc.label = text_sampler_desc.label or \"text-sampler\"\n\tstate.text_sampler = render.sampler(text_sampler_desc)\n\tstate.text_inst = render.buffer {\n\t\ttype = \"vertex\",\n\t\tusage = \"stream\",\n\t\tlabel = \"text-instance\",\n\t\tsize = textmat.instance_size * setting.draw_instance,\n\t}\n\ttext_bindings = render.bindings()\n\ttext_bindings:vbuffer(0, state.text_inst)\n\ttext_bindings:view(0, state.views.storage)\n\ttext_bindings:sampler(0, state.text_sampler)\nelse\n\tstate.text_inst = state.inst\n\ttext_bindings = state.bindings\nend\nstate.text_bindings = text_bindings\nstate.material_text = textmat.normal {\n\tinst_buffer = state.text_inst,\n\tbindings = state.text_bindings,\n\tuniform = state.uniform,\n\tsr_buffer = state.srbuffer_mem,\n\tfont_manager = ctx.font.cobj,\n\ttmp_buffer = ctx.tmp_buffer,\n}\n\nlocal material = {}\n\nfunction material.reset()\n\ttext_bindings:base(0)\nend\n\nfunction material.submit(ptr, n)\n\tstate.material_text:submit(ptr, n)\nend\n\nfunction material.draw(ptr, n)\n\ttext_bindings:view(1, state.views.font)\n\tstate.material_text:draw(ptr, n)\nend\n\nreturn material\n"
  },
  {
    "path": "src/material_blit.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n\n#include \"sokol/sokol_gfx.h\"\n#include \"blit.glsl.h\"\n#include \"render_bindings.h\"\n#include \"material_util.h\"\n\nstruct material_blit {\n\tsg_pipeline pip;\n\tstruct render_bindings *bind;\n};\n\nstatic int\nlmaterial_blit_draw(lua_State *L) {\n\tstruct material_blit *m = (struct material_blit *)luaL_checkudata(L, 1, \"SOLUNA_MATERIAL_BLIT\");\n\tsg_apply_pipeline(m->pip);\n\tsg_apply_bindings(&m->bind->bindings);\n\tsg_draw(0, 4, 1);\n\treturn 0;\n}\n\nstatic void\ninit_pipeline(struct material_blit *p) {\n\tsg_pipeline_desc desc = {\n\t\t.layout.buffers[0].step_func = SG_VERTEXSTEP_PER_VERTEX,\n\t};\n\tp->pip = util_make_pipeline(&desc, blit_shader_desc, \"blit-pipeline\", 0);\n}\n\nstatic int\nlnew_material_blit(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TTABLE);\n\tstruct material_blit *m = (struct material_blit *)lua_newuserdatauv(L, sizeof(*m), 1);\n\tinit_pipeline(m);\n\tutil_ref_object(L, &m->bind, 1, \"bindings\", \"SOKOL_BINDINGS\", 1);\n\t\n\tif (luaL_newmetatable(L, \"SOLUNA_MATERIAL_BLIT\")) {\n\t\tluaL_Reg l[] = {\n\t\t\t{ \"__index\", NULL },\n\t\t\t{ \"draw\", lmaterial_blit_draw },\n\t\t\t{ NULL, NULL },\n\t\t};\n\t\tluaL_setfuncs(L, l, 0);\n\n\t\tlua_pushvalue(L, -1);\n\t\tlua_setfield(L, -2, \"__index\");\n\t}\n\tlua_setmetatable(L, -2);\n\treturn 1;\n}\n\nint\nluaopen_material_blit(lua_State *L) {\n\tluaL_checkversion(L);\n\t\n\tluaL_Reg l[] = {\n\t\t{ \"new\", lnew_material_blit },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\n\treturn 1;\n}\n"
  },
  {
    "path": "src/material_default.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n#include <assert.h>\n#include <stdint.h>\n\n#include \"sokol/sokol_gfx.h\"\n#include \"texquad.glsl.h\"\n#include \"srbuffer.h\"\n#include \"batch.h\"\n#include \"spritemgr.h\"\n#include \"material_util.h\"\n#include \"render_bindings.h\"\n#include \"tmpbuffer.h\"\n\nstruct inst_object {\n\tfloat x, y;\n\tfloat sr_index;\n\tuint32_t offset;\n\tuint32_t u;\n\tuint32_t v;\n};\n\nstruct material_default {\n\tsg_pipeline pip;\n\tsg_buffer inst;\n\tstruct render_bindings *bind;\n\tvs_params_t *uniform;\n\tstruct sr_buffer *srbuffer;\n\tstruct sprite_bank *bank;\n\tstruct tmp_buffer tmp;\n};\n\nstatic void\nsubmit(lua_State *L, void *m_, struct draw_primitive *prim, int n) {\n\tstruct material_default *m = (struct material_default *)m_;\n\tstruct sprite_rect *rect = m->bank->rect;\n\tstruct inst_object *tmp = TMPBUFFER_PTR(struct inst_object, &m->tmp);\n\tint i;\n\tfor (i=0;i<n;i++) {\n\t\tstruct draw_primitive *p = &prim[i];\n\t\t\n\t\t// calc scale/rot index\n\t\tint sr_index = srbuffer_add(m->srbuffer, p->sr);\n\t\tif (sr_index < 0) {\n\t\t\t// todo: support multiply srbuffer\n\t\t\tluaL_error(L, \"sr buffer is full\");\n\t\t}\n\t\ttmp[i].x = (float)p->x / 256.0f;\n\t\ttmp[i].y = (float)p->y / 256.0f;\n\t\ttmp[i].sr_index = (float)sr_index;\n\t\t\n\t\tint index = p->sprite - 1;\n\t\tassert(index >= 0);\n\t\tstruct sprite_rect *r = &rect[index];\n\t\ttmp[i].offset = r->off;\n\t\ttmp[i].u = r->u;\n\t\ttmp[i].v = r->v;\n\t}\n\tsg_append_buffer(m->inst, &(sg_range) { tmp , n * sizeof(tmp[0]) });\n}\n\nstatic int\nlmaterial_default_submit(lua_State *L) {\n\tstruct material_default *m = (struct material_default *)luaL_checkudata(L, 1, \"SOLUNA_MATERIAL_DEFAULT\");\n\tint batch_n = TMPBUFFER_SIZE(struct inst_object, &m->tmp);\n\tutil_submit_material(L, batch_n, m, submit);\n\treturn 0;\n}\n\nstatic inline int\nlmaterial_default_draw_(lua_State *L, int ex) {\n\tstruct material_default *m = (struct material_default *)luaL_checkudata(L, 1, \"SOLUNA_MATERIAL_DEFAULT\");\n//\tstruct draw_primitive *prim = lua_touserdata(L, 2);\n\tint prim_n = luaL_checkinteger(L, 3);\n//\tint tex_id = luaL_checkinteger(L, 4);\n\n\tsg_apply_pipeline(m->pip);\n\tsg_apply_uniforms(UB_vs_params, &(sg_range){ m->uniform, sizeof(vs_params_t) });\n\t\n\tif (ex) {\n\t\tsg_apply_bindings(&m->bind->bindings);\n\t\tsg_draw_ex(0, 4, prim_n, 0, m->bind->base);\n\t} else {\n\t\tsize_t base = m->bind->base * sizeof(struct inst_object);\n\t\tm->bind->bindings.vertex_buffer_offsets[0] += base;\n\t\tsg_apply_bindings(&m->bind->bindings);\n\t\tsg_draw(0, 4, prim_n);\n\t\tm->bind->bindings.vertex_buffer_offsets[0] -= base;\n\t}\n\tm->bind->base += prim_n;\n\n\treturn 0;\n}\n\nstatic int\nlmaterial_default_draw(lua_State *L) {\n\treturn lmaterial_default_draw_(L, 0);\n}\n\nstatic int\nlmaterial_default_draw_ex(lua_State *L) {\n\treturn lmaterial_default_draw_(L, 1);\n}\n\nstatic void\ninit_pipeline(struct material_default *p) {\n\tsg_pipeline_desc desc = {\n\t\t.layout.attrs = {\n\t\t\t[ATTR_texquad_position].format = SG_VERTEXFORMAT_FLOAT3,\n\t\t\t[ATTR_texquad_offset].format = SG_VERTEXFORMAT_UINT,\n\t\t\t[ATTR_texquad_u].format = SG_VERTEXFORMAT_UINT,\n\t\t\t[ATTR_texquad_v].format = SG_VERTEXFORMAT_UINT,\n        },\n\t};\n\tp->pip = util_make_pipeline(&desc, texquad_shader_desc, \"default-pipeline\", 1);\n}\n\nstatic int\nlnew_material_default(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TTABLE);\n\tstruct material_default *m = (struct material_default *)lua_newuserdatauv(L, sizeof(*m), 5);\n\tinit_pipeline(m);\n\tutil_ref_object(L, &m->inst, 1, \"inst_buffer\", \"SOKOL_BUFFER\", 0);\n\tutil_ref_object(L, &m->bind, 2, \"bindings\", \"SOKOL_BINDINGS\", 1);\n\tutil_ref_object(L, &m->uniform, 3, \"uniform\", \"SOKOL_UNIFORM\", 1);\n\tutil_ref_object(L, &m->srbuffer, 4, \"sr_buffer\", \"SOLUNA_SRBUFFER\", 1);\n\ttmp_buffer_init(L, &m->tmp, 5, \"tmp_buffer\");\n\tif (lua_getfield(L, 1, \"sprite_bank\") != LUA_TLIGHTUSERDATA) {\n\t\treturn luaL_error(L, \"Missing .sprite_bank\");\n\t}\n\tm->bank = lua_touserdata(L, -1);\n\tlua_pop(L, 1);\n\t\n\tif (luaL_newmetatable(L, \"SOLUNA_MATERIAL_DEFAULT\")) {\n\t\tluaL_Reg l[] = {\n\t\t\t{ \"__index\", NULL },\n\t\t\t{ \"submit\", lmaterial_default_submit },\n\t\t\t{ \"draw\", DRAWFUNC(lmaterial_default_draw) },\n\t\t\t{ NULL, NULL },\n\t\t};\n\t\tluaL_setfuncs(L, l, 0);\n\n\t\tlua_pushvalue(L, -1);\n\t\tlua_setfield(L, -2, \"__index\");\n\t}\n\tlua_setmetatable(L, -2);\n\treturn 1;\n}\n\nint\nluaopen_material_default(lua_State *L) {\n\tluaL_checkversion(L);\n\tluaL_Reg l[] = {\n\t\t{ \"new\", lnew_material_default },\n\t\t{ \"instance_size\", NULL },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\t\n\tlua_pushinteger(L, sizeof(struct inst_object));\n\tlua_setfield(L, -2, \"instance_size\");\n\treturn 1;\n}\n"
  },
  {
    "path": "src/material_mask.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n#include <assert.h>\n#include <stdint.h>\n\n#include \"sokol/sokol_gfx.h\"\n#include \"maskquad.glsl.h\"\n#include \"srbuffer.h\"\n#include \"batch.h\"\n#include \"spritemgr.h\"\n#include \"material_util.h\"\n#include \"render_bindings.h\"\n#include \"tmpbuffer.h\"\n\nstruct color {\n\tunsigned char channel[4];\n};\n\nstruct inst_object {\n\tfloat x, y;\n\tfloat sr_index;\n\tstruct color maskcolor;\n\tuint32_t offset;\n\tuint32_t u;\n\tuint32_t v;\n};\n\nstruct mask {\n\tstruct draw_primitive_external header;\n\tstruct color c;\n};\n\nstruct material_mask {\n\tsg_pipeline pip;\n\tsg_buffer inst;\n\tstruct render_bindings *bind;\n\tvs_params_t *uniform;\n\tstruct sr_buffer *srbuffer;\n\tstruct sprite_bank *bank;\n\tstruct tmp_buffer tmp;\n};\n\nstatic int material_id = 0;\n\nstatic void\nsubmit(lua_State *L, void *m_, struct draw_primitive *prim, int n) {\n\tstruct material_mask *m =(struct material_mask *)m_;\n\tstruct sprite_rect *rect = m->bank->rect;\n\tstruct inst_object *tmp = TMPBUFFER_PTR(struct inst_object, &m->tmp);\n\tint i;\n\tfor (i=0;i<n;i++) {\n\t\tstruct draw_primitive *p = &prim[i*2];\n\t\tassert(p->sprite == -material_id);\n\t\t\n\t\tstruct mask * mask = (struct mask *)&prim[i*2+1];\n\n\t\t// calc scale/rot index\n\t\tint sr_index = srbuffer_add(m->srbuffer, p->sr);\n\t\tif (sr_index < 0) {\n\t\t\t// todo: support multiply srbuffer\n\t\t\tluaL_error(L, \"sr buffer is full\");\n\t\t}\n\t\ttmp[i].x = (float)p->x / 256.0f;\n\t\ttmp[i].y = (float)p->y / 256.0f;\n\t\ttmp[i].sr_index = (float)sr_index;\n\t\ttmp[i].maskcolor = mask->c;\n\t\t\n\t\tint index = mask->header.sprite;\n\t\tassert(index >= 0);\n\t\tstruct sprite_rect *r = &rect[index];\n\t\ttmp[i].offset = r->off;\n\t\ttmp[i].u = r->u;\n\t\ttmp[i].v = r->v;\n\t}\n\tsg_append_buffer(m->inst, &(sg_range) { tmp , n * sizeof(tmp[0]) });\n}\n\nstatic int\nlmaterial_mask_submit(lua_State *L) {\n\tstruct material_mask *m = (struct material_mask *)luaL_checkudata(L, 1, \"SOLUNA_MATERIAL_MASK\");\n\tint batch_n = TMPBUFFER_SIZE(struct inst_object, &m->tmp);\n\tutil_submit_material(L, batch_n, m, submit);\n\treturn 0;\n}\n\nstatic inline int\nlmaterial_mask_draw_(lua_State *L, int ex) {\n\tstruct material_mask *m = (struct material_mask *)luaL_checkudata(L, 1, \"SOLUNA_MATERIAL_MASK\");\n//\tstruct draw_primitive *prim = lua_touserdata(L, 2);\n\tint prim_n = luaL_checkinteger(L, 3);\n//\tint tex_id = luaL_checkinteger(L, 4);\n\n\tsg_apply_pipeline(m->pip);\n\tsg_apply_uniforms(UB_vs_params, &(sg_range){ m->uniform, sizeof(vs_params_t) });\n\t\n\tif (ex) {\n\t\tsg_apply_bindings(&m->bind->bindings);\n\t\tsg_draw_ex(0, 4, prim_n, 0, m->bind->base);\n\t} else {\n\t\tsize_t base = m->bind->base * sizeof(struct inst_object);\n\t\tm->bind->bindings.vertex_buffer_offsets[0] += base;\n\t\tsg_apply_bindings(&m->bind->bindings);\n\t\tsg_draw(0, 4, prim_n);\n\t\tm->bind->bindings.vertex_buffer_offsets[0] -= base;\n\t}\n\n\tm->bind->base += prim_n;\n\n\treturn 0;\n}\n\nstatic int\nlmaterial_mask_draw(lua_State *L) {\n\treturn lmaterial_mask_draw_(L, 0);\n}\n\nstatic int\nlmaterial_mask_draw_ex(lua_State *L) {\n\treturn lmaterial_mask_draw_(L, 1);\n}\n\nstatic int\nlset_material_id(lua_State *L) {\n\tint id = luaL_checkinteger(L, 1);\n\tif (id <= 0) {\n\t\treturn luaL_error(L, \"Invalid mask material id %d\", id);\n\t}\n\tmaterial_id = id;\n\treturn 0;\n}\n\nstatic void\ninit_pipeline(struct material_mask *p) {\n\tsg_pipeline_desc desc = {\n\t\t.layout.attrs = {\n\t\t\t[ATTR_maskquad_position].format = SG_VERTEXFORMAT_FLOAT3,\n\t\t\t[ATTR_maskquad_color].format = SG_VERTEXFORMAT_UBYTE4N,\n\t\t\t[ATTR_maskquad_offset].format = SG_VERTEXFORMAT_UINT,\n\t\t\t[ATTR_maskquad_u].format = SG_VERTEXFORMAT_UINT,\n\t\t\t[ATTR_maskquad_v].format = SG_VERTEXFORMAT_UINT,\n        },\n    };\n\tp->pip = util_make_pipeline(&desc, maskquad_shader_desc, \"mask-pipeline\", 1);\n}\n\nstatic int\nlnew_material_mask(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TTABLE);\n\tstruct material_mask *m = (struct material_mask *)lua_newuserdatauv(L, sizeof(*m), 5);\n\tinit_pipeline(m);\n\tutil_ref_object(L, &m->inst, 1, \"inst_buffer\", \"SOKOL_BUFFER\", 0);\n\tutil_ref_object(L, &m->bind, 2, \"bindings\", \"SOKOL_BINDINGS\", 1);\n\tutil_ref_object(L, &m->uniform, 3, \"uniform\", \"SOKOL_UNIFORM\", 1);\n\tutil_ref_object(L, &m->srbuffer, 4, \"sr_buffer\", \"SOLUNA_SRBUFFER\", 1);\n\ttmp_buffer_init(L, &m->tmp, 5, \"tmp_buffer\");\n\tif (lua_getfield(L, 1, \"sprite_bank\") != LUA_TLIGHTUSERDATA) {\n\t\treturn luaL_error(L, \"Missing .sprite_bank\");\n\t}\n\tm->bank = lua_touserdata(L, -1);\n\tlua_pop(L, 1);\n\t\n\tif (luaL_newmetatable(L, \"SOLUNA_MATERIAL_MASK\")) {\n\t\tluaL_Reg l[] = {\n\t\t\t{ \"__index\", NULL },\n\t\t\t{ \"submit\", lmaterial_mask_submit },\n\t\t\t{ \"draw\", DRAWFUNC(lmaterial_mask_draw) },\n\t\t\t{ NULL, NULL },\n\t\t};\n\t\tluaL_setfuncs(L, l, 0);\n\n\t\tlua_pushvalue(L, -1);\n\t\tlua_setfield(L, -2, \"__index\");\n\t}\n\tlua_setmetatable(L, -2);\n\treturn 1;\n}\n\nstruct mask_primitive {\n\tstruct draw_primitive pos;\n\tunion {\n\t\tstruct draw_primitive dummy;\n\t\tstruct mask m;\n\t} u;\n};\n\nstatic int\nlmask(lua_State *L) {\n\tif (material_id <= 0) {\n\t\treturn luaL_error(L, \"Mask material is not registered\");\n\t}\n\tstruct mask_primitive prim;\n\tprim.pos.x = 0;\n\tprim.pos.y = 0;\n\tprim.pos.sr = 0;\n\tprim.pos.sprite = -material_id;\n\tprim.u.m.header.sprite = luaL_checkinteger(L, 1) - 1;\n\tuint32_t color = luaL_checkinteger(L, 2);\n\tif (!(color & 0xff000000))\n\t\tcolor |= 0xff000000;\n\tprim.u.m.c.channel[0] = (color >> 16) & 0xff;\n\tprim.u.m.c.channel[1] = (color >> 8) & 0xff;\n\tprim.u.m.c.channel[2] = color & 0xff;\n\tprim.u.m.c.channel[3] = (color >> 24) & 0xff;\n\tlua_pushlstring(L, (const char *)&prim, sizeof(prim));\n\treturn 1;\n}\n\nint\nluaopen_material_mask(lua_State *L) {\n\tluaL_checkversion(L);\n\tluaL_Reg l[] = {\n\t\t{ \"set_material_id\", lset_material_id },\n\t\t{ \"mask\", lmask },\n\t\t{ \"new\", lnew_material_mask },\n\t\t{ \"instance_size\", NULL },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\t\n\tlua_pushinteger(L, sizeof(struct inst_object));\n\tlua_setfield(L, -2, \"instance_size\");\n\treturn 1;\n}\n"
  },
  {
    "path": "src/material_quad.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n#include <assert.h>\n#include <string.h>\n\n#include \"sokol/sokol_gfx.h\"\n#include \"colorquad.glsl.h\"\n#include \"srbuffer.h\"\n#include \"batch.h\"\n#include \"spritemgr.h\"\n#include \"material_util.h\"\n#include \"render_bindings.h\"\n#include \"tmpbuffer.h\"\n\nstruct color {\n\tunsigned char channel[4];\n};\n\nstruct quad {\n\tstruct draw_primitive_external header;\n\tfloat w;\n\tfloat h;\n\tstruct color c;\n};\n\nstruct inst_object {\n\tfloat x, y;\n\tfloat w, h;\n\tint sr_index;\n\tstruct color c;\n};\n\nstruct material_quad {\n\tsg_pipeline pip;\n\tsg_buffer inst;\n\tstruct render_bindings *bind;\n\tvs_params_t *uniform;\n\tstruct sr_buffer *srbuffer;\n\tstruct tmp_buffer tmp;\n};\n\nstatic int material_id = 0;\n\nstatic void\nsubmit(lua_State *L, void *m_, struct draw_primitive *prim, int n) {\n\tstruct material_quad *m = (struct material_quad *)m_;\n\tstruct inst_object *tmp = TMPBUFFER_PTR(struct inst_object, &m->tmp);\n\tint i;\n\tfor (i=0;i<n;i++) {\n\t\tstruct draw_primitive *p = &prim[i*2];\n\t\tassert(p->sprite == -material_id);\n\t\t\n\t\tstruct quad * q = (struct quad *)&prim[i*2+1];\n\t\t\n\t\t// calc scale/rot index\n\t\tint sr_index = srbuffer_add(m->srbuffer, p->sr);\n\t\tif (sr_index < 0) {\n\t\t\t// todo: support multiply srbuffer\n\t\t\tluaL_error(L, \"sr buffer is full\");\n\t\t}\n\t\tstruct inst_object *inst = &tmp[i];\n\t\tinst->x = (float)p->x / 256.0f;\n\t\tinst->y = (float)p->y / 256.0f;\n\t\tinst->w = q->w;\n\t\tinst->h = q->h;\n\t\tinst->sr_index = sr_index;\n\t\tinst->c = q->c;\n\t}\n\tsg_append_buffer(m->inst, &(sg_range) { tmp , n * sizeof(tmp[0]) });\n}\n\nstatic int\nlmateraial_quad_submit(lua_State *L) {\n\tstruct material_quad *m = (struct material_quad *)luaL_checkudata(L, 1, \"SOLUNA_MATERIAL_QUAD\");\n\tint batch_n = TMPBUFFER_SIZE(struct inst_object, &m->tmp);\n\tutil_submit_material(L, batch_n, m, submit);\n\treturn 0;\n}\n\nstatic inline int\nlmateraial_quad_draw_(lua_State *L, int ex) {\n\tstruct material_quad *m = (struct material_quad *)luaL_checkudata(L, 1, \"SOLUNA_MATERIAL_QUAD\");\n//\tstruct draw_primitive *prim = lua_touserdata(L, 2);\n\tint prim_n = luaL_checkinteger(L, 3);\n\tif (prim_n <= 0)\n\t\treturn 0;\n\t\n\tsg_apply_pipeline(m->pip);\n\tsg_apply_uniforms(UB_vs_params, &(sg_range){ m->uniform, sizeof(vs_params_t) });\n\tif (ex) {\n\t\tsg_apply_bindings(&m->bind->bindings);\n\t\tsg_draw_ex(0, 4, prim_n, 0, m->bind->base);\n\t} else {\n\t\tsize_t base = m->bind->base * sizeof(struct inst_object);\n\t\tm->bind->bindings.vertex_buffer_offsets[0] += base;\n\t\tsg_apply_bindings(&m->bind->bindings);\n\t\tsg_draw(0, 4, prim_n);\n\t\tm->bind->bindings.vertex_buffer_offsets[0] -= base;\n\t}\n\n\tm->bind->base += prim_n;\n\n\treturn 0;\n}\n\nstatic int\nlmateraial_quad_draw(lua_State *L) {\n\treturn lmateraial_quad_draw_(L, 0);\n}\n\nstatic int\nlmateraial_quad_draw_ex(lua_State *L) {\n\treturn lmateraial_quad_draw_(L, 1);\n}\n\nstatic int\nlset_material_id(lua_State *L) {\n\tint id = luaL_checkinteger(L, 1);\n\tif (id <= 0) {\n\t\treturn luaL_error(L, \"Invalid quad material id %d\", id);\n\t}\n\tmaterial_id = id;\n\treturn 0;\n}\n\nstatic void\ninit_pipeline(struct material_quad *p) {\n\tsg_pipeline_desc desc = {\n\t\t.layout.attrs = {\n\t\t\t[ATTR_colorquad_position].format = SG_VERTEXFORMAT_FLOAT4,\n\t\t\t[ATTR_colorquad_idx].format = SG_VERTEXFORMAT_UINT,\n\t\t\t[ATTR_colorquad_c].format = SG_VERTEXFORMAT_UBYTE4N,\n        },\n    };\n\tp->pip = util_make_pipeline(&desc, colorquad_shader_desc, \"colorquad-pipeline\", 1);\n}\n\nstatic int\nlnew_material_quad(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TTABLE);\n\tstruct material_quad *m = (struct material_quad *)lua_newuserdatauv(L, sizeof(*m), 5);\n\tutil_ref_object(L, &m->inst, 1, \"inst_buffer\", \"SOKOL_BUFFER\", 0);\n\tutil_ref_object(L, &m->bind, 2, \"bindings\", \"SOKOL_BINDINGS\", 1);\n\tutil_ref_object(L, &m->uniform, 3, \"uniform\", \"SOKOL_UNIFORM\", 1);\n\tutil_ref_object(L, &m->srbuffer, 4, \"sr_buffer\", \"SOLUNA_SRBUFFER\", 1);\n\ttmp_buffer_init(L, &m->tmp, 5, \"tmp_buffer\");\n\tinit_pipeline(m);\n\n\tif (luaL_newmetatable(L, \"SOLUNA_MATERIAL_QUAD\")) {\n\t\tluaL_Reg l[] = {\n\t\t\t{ \"__index\", NULL },\n\t\t\t{ \"submit\", lmateraial_quad_submit },\n\t\t\t{ \"draw\", DRAWFUNC(lmateraial_quad_draw) },\n\t\t\t{ NULL, NULL },\n\t\t};\n\t\tluaL_setfuncs(L, l, 0);\n\n\t\tlua_pushvalue(L, -1);\n\t\tlua_setfield(L, -2, \"__index\");\n\t}\n\tlua_setmetatable(L, -2);\n\treturn 1;\n}\n\nstruct quad_primitive {\n\tstruct draw_primitive pos;\n\tunion {\n\t\tstruct draw_primitive dummy;\n\t\tstruct quad q;\n\t} u;\n};\n\nstatic int\nlquad(lua_State *L) {\n\tif (material_id <= 0) {\n\t\treturn luaL_error(L, \"Quad material is not registered\");\n\t}\n\tstruct quad_primitive prim;\n\tprim.pos.x = 0;\n\tprim.pos.y = 0;\n\tprim.pos.sr = 0;\n\tprim.pos.sprite = -material_id;\n\tprim.u.q.w = luaL_checkinteger(L, 1);\n\tprim.u.q.h = luaL_checkinteger(L, 2);\n\tuint32_t color = luaL_checkinteger(L, 3);\n\tif (!(color & 0xff000000))\n\t\tcolor |= 0xff000000;\n\tprim.u.q.header.sprite = -1;\n\tprim.u.q.c.channel[0] = (color >> 16) & 0xff;\n\tprim.u.q.c.channel[1] = (color >> 8) & 0xff;\n\tprim.u.q.c.channel[2] = color & 0xff;\n\tprim.u.q.c.channel[3] = (color >> 24) & 0xff;\n\tlua_pushlstring(L, (const char *)&prim, sizeof(prim));\n\treturn 1;\n}\n\nint\nluaopen_material_quad(lua_State *L) {\n\tluaL_checkversion(L);\n\tluaL_Reg l[] = {\n\t\t{ \"set_material_id\", lset_material_id },\n\t\t{ \"quad\", lquad },\n\t\t{ \"new\", lnew_material_quad },\n\t\t{ \"instance_size\", NULL },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\n\tlua_pushinteger(L, sizeof(struct inst_object));\n\tlua_setfield(L, -2, \"instance_size\");\n\t\n\treturn 1;\n}\n"
  },
  {
    "path": "src/material_text.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n#include <assert.h>\n#include <string.h>\n#include <stdio.h>\n#include <stdint.h>\n#include <stdlib.h>\n\n#include \"sokol/sokol_gfx.h\"\n#include \"sdftext.glsl.h\"\n#include \"srbuffer.h\"\n#include \"batch.h\"\n#include \"spritemgr.h\"\n#include \"font_manager.h\"\n#include \"sprite_submit.h\"\n#include \"material_util.h\"\n#include \"render_bindings.h\"\n#include \"tmpbuffer.h\"\n\nstruct text {\n\tstruct draw_primitive_external header;\n\tint codepoint;\n\tuint16_t font;\n\tuint16_t size;\n\tuint32_t color;\n};\n\nstruct inst_object {\n\tfloat x, y;\n\tfloat sr_index;\n    uint32_t offset;\n    uint32_t u;\n    uint32_t v;\n};\n\nstruct material_text {\n\tsg_pipeline pip;\n\tsg_buffer inst;\n\tstruct render_bindings *bind;\n\tvs_params_t *uniform;\n\tstruct sr_buffer *srbuffer;\n\tstruct font_manager *font;\n\tfs_params_t fs_uniform;\n\tstruct tmp_buffer tmp;\n};\n\nstatic int material_id = 0;\n\nstatic void\nsubmit(lua_State *L, void *m_, struct draw_primitive *prim, int n) {\n\tstruct material_text *m = (struct material_text *)m_;\n\tstruct inst_object *tmp = TMPBUFFER_PTR(struct inst_object, &m->tmp);\n\tint i;\n\tint count = 0;\n\tfor (i=0;i<n;i++) {\n\t\tstruct draw_primitive *p = &prim[i*2];\n\t\tassert(p->sprite == -material_id);\n\t\t\n\t\tstruct text * t = (struct text *)&prim[i*2+1];\n\t\tstruct font_glyph g, og;\n\t\tconst char* err = font_manager_glyph(m->font, t->font, t->codepoint, t->size, &g, &og);\n\t\tif (err == NULL) {\n\t\t\ttmp[count].offset = (-og.offset_x + 0x8000) << 16 | (-og.offset_y + 0x8000);\n\t\t\ttmp[count].u = og.u << 16 | FONT_MANAGER_GLYPHSIZE;\n\t\t\ttmp[count].v = og.v << 16 | FONT_MANAGER_GLYPHSIZE;\n\t\t\t\n\t\t\tuint32_t scale_fix = og.w == 0 ? 0 : (g.w << 12) / og.w;\n\t\t\tsprite_apply_scale(p, scale_fix);\n\t\t\t// calc scale/rot index\n\t\t\tint sr_index = srbuffer_add(m->srbuffer, p->sr);\n\t\t\tif (sr_index < 0) {\n\t\t\t\t// todo: support multiply srbuffer\n\t\t\t\tluaL_error(L, \"sr buffer is full\");\n\t\t\t}\n\t\t\ttmp[count].x = (float)p->x / 256.0f;\n\t\t\ttmp[count].y = (float)p->y / 256.0f;\n\t\t\ttmp[count].sr_index = (float)sr_index;\n\t\t\t++count;\n\t\t} else {\n\t\t\tt->codepoint = -1;\n\t\t}\n\t}\n\tsg_append_buffer(m->inst, &(sg_range) { tmp , count * sizeof(tmp[0]) });\n}\n\nstatic int\nlmateraial_text_submit(lua_State *L) {\n\tstruct material_text *m = (struct material_text *)luaL_checkudata(L, 1, \"SOLUNA_MATERIAL_TEXT\");\n\tint batch_n = TMPBUFFER_SIZE(struct inst_object, &m->tmp);\n\tutil_submit_material(L, batch_n, m, submit);\n\treturn 0;\n}\n\nstatic inline void\ndraw_text(struct material_text *m, uint32_t color, int count, int ex) {\n\tm->fs_uniform.color = color;\n\tsg_apply_uniforms(UB_vs_params, &(sg_range){ m->uniform, sizeof(vs_params_t) });\n\tsg_apply_uniforms(UB_fs_params, &(sg_range){ &m->fs_uniform, sizeof(fs_params_t) });\n\tif (ex) {\n\t\tsg_apply_bindings(&m->bind->bindings);\n\t\tsg_draw_ex(0, 4, count, 0, m->bind->base);\n\t} else {\n\t\tsize_t base = m->bind->base * sizeof(struct inst_object);\n\t\tm->bind->bindings.vertex_buffer_offsets[0] += base;\n\t\tsg_apply_bindings(&m->bind->bindings);\n\t\tsg_draw(0, 4, count);\n\t\tm->bind->bindings.vertex_buffer_offsets[0] -= base;\n\t}\n\n\tm->bind->base += count;\n}\n\nstatic inline int\nlmateraial_text_draw_(lua_State *L, int ex) {\n\tstruct material_text *m = (struct material_text *)luaL_checkudata(L, 1, \"SOLUNA_MATERIAL_TEXT\");\n\tstruct draw_primitive *prim = lua_touserdata(L, 2);\n\tint prim_n = luaL_checkinteger(L, 3);\n\tif (prim_n <= 0)\n\t\treturn 0;\n\t\n\tint i;\n\tfloat texsize = m->uniform->texsize;\n\tm->uniform->texsize = 1.0f / FONT_MANAGER_TEXSIZE;\n\tsg_apply_pipeline(m->pip);\n\t\n\tint count = -1;\n\tuint32_t color = 0;\n\tfor (i=0;i<prim_n;i++) {\n\t\tstruct text * t = (struct text *)&prim[i*2+1];\n\t\tif (t->codepoint >= 0) {\n\t\t\tif (count < 0) {\n\t\t\t\tcolor = t->color;\n\t\t\t\tcount = 1;\n\t\t\t} else if (t->color != color) {\n\t\t\t\tdraw_text(m, color, count, ex);\n\t\t\t\tcolor = t->color;\n\t\t\t\tcount = 1;\n\t\t\t} else {\n\t\t\t\t++count;\n\t\t\t}\n\t\t}\n\t}\n\tdraw_text(m, color, count, ex);\n\n\tm->uniform->texsize = texsize;\n\n\treturn 0;\n}\n\nstatic int\nlmateraial_text_draw(lua_State *L) {\n\treturn lmateraial_text_draw_(L, 0);\n}\n\nstatic int\nlmateraial_text_draw_ex(lua_State *L) {\n\treturn lmateraial_text_draw_(L, 1);\n}\n\nstatic void\ninit_pipeline(struct material_text *p) {\n\tsg_pipeline_desc desc = {\n\t\t.layout.attrs = {\n\t\t\t[ATTR_texquad_position].format = SG_VERTEXFORMAT_FLOAT3,\n\t\t\t[ATTR_texquad_offset].format = SG_VERTEXFORMAT_UINT,\n\t\t\t[ATTR_texquad_u].format = SG_VERTEXFORMAT_UINT,\n\t\t\t[ATTR_texquad_v].format = SG_VERTEXFORMAT_UINT,\n        },\n    };\n\tp->pip = util_make_pipeline(&desc, texquad_shader_desc, \"text-pipeline\", 1);\n}\n\nstatic int\nlnew_material_text_normal(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TTABLE);\n\tstruct material_text *m = (struct material_text *)lua_newuserdatauv(L, sizeof(*m), 5);\n\tutil_ref_object(L, &m->inst, 1, \"inst_buffer\", \"SOKOL_BUFFER\", 0);\n\tutil_ref_object(L, &m->bind, 2, \"bindings\", \"SOKOL_BINDINGS\", 1);\n\tutil_ref_object(L, &m->uniform, 3, \"uniform\", \"SOKOL_UNIFORM\", 1);\n\tutil_ref_object(L, &m->srbuffer, 4, \"sr_buffer\", \"SOLUNA_SRBUFFER\", 1);\n\ttmp_buffer_init(L, &m->tmp, 5, \"tmp_buffer\");\n\tinit_pipeline(m);\n\n\tif (lua_getfield(L, 1, \"font_manager\") != LUA_TLIGHTUSERDATA) {\n\t\treturn luaL_error(L, \"Missing .font_manager\");\n\t}\n\tm->font = lua_touserdata(L, -1);\n\tlua_pop(L, 1);\n\t\n  fs_params_t temp = {\n      .edge_mask = font_manager_sdf_mask(m->font),\n      .dist_multiplier = 1.0f,\n      .color = 0xff000000,\n  };\n  memcpy(&m->fs_uniform, &temp, sizeof(fs_params_t));\n\n\tif (luaL_newmetatable(L, \"SOLUNA_MATERIAL_TEXT\")) {\n\t\tluaL_Reg l[] = {\n\t\t\t{ \"__index\", NULL },\n\t\t\t{ \"submit\", lmateraial_text_submit },\n\t\t\t{ \"draw\", DRAWFUNC(lmateraial_text_draw) },\n\t\t\t{ NULL, NULL },\n\t\t};\n\t\tluaL_setfuncs(L, l, 0);\n\n\t\tlua_pushvalue(L, -1);\n\t\tlua_setfield(L, -2, \"__index\");\n\t}\n\tlua_setmetatable(L, -2);\n\treturn 1;\n}\n\nstatic int\nlchar_for_batch(lua_State *L) {\n\tif (material_id <= 0) {\n\t\treturn luaL_error(L, \"Text material is not registered\");\n\t}\n\tstruct text * t = (struct text *)lua_touserdata(L, lua_upvalueindex(1));\n\tt->header.sprite = -1;\n\tt->codepoint = luaL_checkinteger(L, 1);\n\tt->font = luaL_checkinteger(L, 2);\n\tt->size = luaL_checkinteger(L, 3);\n\tt->color = luaL_checkinteger(L, 4);\n\tif (!(t->color & 0xff000000))\n\t\tt->color |= 0xff000000;\n\tlua_pushvalue(L, lua_upvalueindex(1));\n\treturn 1;\n}\n\nstatic int\nlset_material_id(lua_State *L) {\n\tint id = luaL_checkinteger(L, 1);\n\tif (id <= 0) {\n\t\treturn luaL_error(L, \"Invalid text material id %d\", id);\n\t}\n\tmaterial_id = id;\n\tlua_pushvalue(L, lua_upvalueindex(1));\n\tlua_pushinteger(L, id);\n\tlua_setiuservalue(L, -2, 1);\n\tlua_pop(L, 1);\n\treturn 0;\n}\n\nstruct text_primitive {\n\tstruct draw_primitive pos;\n\tunion {\n\t\tstruct draw_primitive dummy;\n\t\tstruct text text;\n\t} u;\n};\n\n/*\n** From lua utf8lib :\n** Decode one UTF-8 sequence, returning NULL if byte sequence is\n** invalid.  The array 'limits' stores the minimum value for each\n** sequence length, to check for overlong representations. Its first\n** entry forces an error for non-ascii bytes with no continuation\n** bytes (count == 0).\n*/\n\n#define iscont(c)\t(((c) & 0xC0) == 0x80)\n#define l_uint32 uint32_t\n#define MAXUTF\t\t0x7FFFFFFFu\n\nstatic const char *utf8_decode (const char *s, l_uint32 *val) {\n  static const l_uint32 limits[] =\n        {~(l_uint32)0, 0x80, 0x800, 0x10000u, 0x200000u, 0x4000000u};\n  unsigned int c = (unsigned char)s[0];\n  l_uint32 res = 0;  /* final result */\n  if (c < 0x80)  /* ascii? */\n    res = c;\n  else {\n    int count = 0;  /* to count number of continuation bytes */\n    for (; c & 0x40; c <<= 1) {  /* while it needs continuation bytes... */\n      unsigned int cc = (unsigned char)s[++count];  /* read next byte */\n      if (!iscont(cc))  /* not a continuation byte? */\n        return NULL;  /* invalid byte sequence */\n      res = (res << 6) | (cc & 0x3F);  /* add lower 6 bits from cont. byte */\n    }\n    res |= ((l_uint32)(c & 0x7F) << (count * 5));  /* add first byte */\n    if (count > 5 || res > MAXUTF || res < limits[count])\n      return NULL;  /* invalid byte sequence */\n    s += count;  /* skip continuation bytes read */\n  }\n  *val = res;\n  return s + 1;  /* +1 to include first byte */\n}\n\nstatic const char *\nskip_bracket(const char *str) {\n\tfor (;;) {\n\t\tif (*str == ']') {\n\t\t\treturn str + 1;\n\t\t} else if (*str == '\\0') {\n\t\t\treturn str;\n\t\t}\n\t\t++str;\n\t}\n}\n\nstatic int\ncount_string(const char *str) {\n\tuint32_t val = 0;\n\tint n = 0;\n\twhile ((str = utf8_decode(str, &val))) {\n\t\tif (val == 0)\n\t\t\tbreak;\n\t\tif (val > 32) {\n\t\t\tif (val == '[') {\n\t\t\t\tchar c = *str;\n\t\t\t\tif (c == '[') {\n\t\t\t\t\t++str;\n\t\t\t\t\t++n;\n\t\t\t\t} else {\n\t\t\t\t\tif (c == 'i') {\n\t\t\t\t\t\t// icons\n\t\t\t\t\t\t++n;\n\t\t\t\t\t}\n\t\t\t\t\tstr = skip_bracket(str);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t++n;\n\t\t\t}\n\t\t}\n\t}\n\treturn n;\n}\n\n#define MAX_WIDTH 4096\n#define MAX_HEIGHT 4096\n#define DEFAULT_FONTSIZE 24\n\nstatic void *\nfree_primitive(void *ud, void *ptr, size_t osize, size_t nsize) {\n\tfree(ptr);\n\treturn NULL;\n}\n\n#define ALIGNMENT_LEFT 0\n#define ALIGNMENT_CENTER 1\n#define ALIGNMENT_RIGHT 2\n#define ALIGNMENT_MASK 3\n#define VALIGNMENT_TOP (1<<2)\n#define VALIGNMENT_CENTER 0\n#define VALIGNMENT_BOTTOM (2<<2)\n#define VALIGNMENT_MASK (3<<2)\n\n// todo: support multi font/size\nstruct block_context {\n\tint width;\n\tint height;\n\tint x;\n\tint y;\n\tint ascent;\n\tint decent;\n\tint line_prim;\n\tint line_width;\n\tint alignment;\n\tuint32_t default_color;\n\tuint32_t color;\n};\n\nstatic inline int\nadvance(struct block_context *ctx, int x) {\n\tif (x + ctx->x > ctx->width)\n\t\treturn 1;\n\tctx->x += x;\n\treturn 0;\n}\n\nstruct position {\n\t// input\n\tint n;\n\t// output\n\tint x;\n\tint y;\n\tint w;\n\tint h;\n\tint decent;\n};\n\nstatic inline int\nnewline(struct block_context *ctx, struct text_primitive * prim, int n, struct position *pos) {\n\tint from = ctx->line_prim;\n\tctx->line_prim = n;\n\tint line_width = ctx->line_width;\n\tctx->line_width = 0;\n\tint offx = 0;\n\tint align = ctx->alignment & ALIGNMENT_MASK;\n\tswitch (align) {\n\tcase ALIGNMENT_CENTER:\n\t\toffx = (ctx->width - line_width) / 2 * 256;\n\t\tbreak;\n\tcase ALIGNMENT_RIGHT:\n\t\toffx = (ctx->width - line_width) * 256;\n\t\tbreak;\n\t}\n\tif (pos && pos->n <= 0 && pos->y == ctx->y) {\n\t\tpos->x += offx / 256;\n\t}\n\t\n\tif (prim != NULL && offx > 0) {\n\t\tint i;\n\t\tfor (i=from;i<n;i++) {\n\t\t\tprim[i].pos.x += offx;\n\t\t}\n\t}\n\n\tif (ctx->y + ctx->ascent + ctx->decent > ctx->height) {\n\t\treturn 1;\n\t} else {\n\t\tctx->y += ctx->ascent + ctx->decent;\n\t\tctx->x = 0;\n\n\t\treturn 0;\n\t}\n}\n\nstatic inline int\ntohex(char c) {\n\tif (c >= '0' && c <= '9')\n\t\treturn c - '0';\n\tif (c >= 'A' && c <= 'F')\n\t\treturn c - 'A' + 10;\n\treturn -1;\n}\n\nstatic const char *\nparse_bracket(struct block_context *ctx, const char *str, int *icon) {\n\tchar c = *str;\n\tint hex = -1;\n\tif (c == 'i') {\n\t\t++str;\n\t\tint num = 0;\n\t\twhile (*str >= '0' && *str <= '9') {\n\t\t\tnum = num * 10 + (*str - '0');\n\t\t\t++str;\n\t\t}\n\t\t*icon = num + 1;\n\t} else if ((hex = tohex(c)) >= 0) {\n\t\tint color = hex;\n\t\tfor (;;) {\n\t\t\t++str;\n\t\t\tif ((hex = tohex(*str)) >= 0) {\n\t\t\t\tcolor = color * 16 + hex;\n\t\t\t} else {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (!(color & 0xff000000))\n\t\t\tcolor |= 0xff000000;\n\t\tctx->color = color;\n\t} else if (c == 'n') {\n\t\tctx->color = ctx->default_color;\n\t}\n\t// todo: other command\n\treturn skip_bracket(str);\n}\n\nstatic inline void\nadvance_position(struct position *pos, struct block_context *ctx) {\n\tif (pos) {\n\t\tif (pos->n == 0) {\n\t\t\tpos->x = ctx->x;\n\t\t\tpos->y = ctx->y;\n\t\t}\n\t\t--pos->n;\n\t}\n}\n\n// todo: support color\nstatic int\nltext_(lua_State *L, struct position *pos) {\n\tconst char * str = luaL_checkstring(L, 1);\n\tint count = count_string(str);\n\tstruct block_context ctx;\n\tctx.width = luaL_optinteger(L, 2, MAX_WIDTH);\n\tctx.height = luaL_optinteger(L, 3, MAX_HEIGHT);\n\tstruct font_manager *mgr = (struct font_manager *)lua_touserdata(L, lua_upvalueindex(1));\n\tint fontid = lua_tointeger(L, lua_upvalueindex(2));\n\tint fontsize = lua_tointeger(L, lua_upvalueindex(3));\n\tctx.default_color = lua_tointeger(L, lua_upvalueindex(4));\n\tctx.color = ctx.default_color;\n\tctx.x = 0;\n\tint decent, gap;\n\tfont_manager_fontheight(mgr, fontid, fontsize, &ctx.ascent, &decent, &gap);\n\tif (gap == 0)\n\t\tgap = 1;\n\tctx.decent = -decent + gap;\n\tctx.y = ctx.ascent;\n\tctx.line_prim = 0;\n\tctx.line_width = 0;\n\tctx.alignment = lua_tointeger(L, lua_upvalueindex(5));\n\tint pos_n = 0;\n\tif (pos) {\n\t\tif (pos->n < 0) {\n\t\t\tpos->n = 0;\n\t\t}\n\t\tpos_n = pos->n;\n\t}\n\t\n\tchar * buffer = NULL;\n\tstruct text_primitive * prim = NULL;\n\tif (pos == NULL) {\n\t\tbuffer = (char *)malloc(count * sizeof(struct text_primitive)+1);\n\t\tprim = (struct text_primitive *)buffer;\n\t}\n\tint i;\n\tint n = 0;\n\tfor (i=0;i<count;) {\n\t\tuint32_t val = 0;\n\t\tstr = utf8_decode(str, &val);\n\t\tif (val <= 32) {\n\t\t\tadvance_position(pos, &ctx);\n\t\t\tif (val == '\\n') {\n\t\t\t\tif (ctx.x > ctx.line_width)\n\t\t\t\t\tctx.line_width = ctx.x;\n\t\t\t\tif (newline(&ctx, prim, n, pos))\n\t\t\t\t\tbreak;\n\t\t\t} else {\n\t\t\t\tstruct font_glyph g, og;\n\t\t\t\tif (font_manager_glyph(mgr, fontid, ' ', fontsize, &g, &og) == NULL) {\n\t\t\t\t\tif (ctx.x > ctx.line_width)\n\t\t\t\t\t\tctx.line_width = ctx.x;\n\t\t\t\t\tif (advance(&ctx, g.advance_x)) {\n\t\t\t\t\t\tif (newline(&ctx, prim, n, pos))\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tadvance(&ctx, g.advance_x);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tint icon = 0;\n\t\t\tif (val == '[') {\n\t\t\t\tif (*str != '[') {\n\t\t\t\t\tstr = parse_bracket(&ctx, str, &icon);\n\t\t\t\t\tif (!icon) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t++str;\n\t\t\t\t}\n\t\t\t}\n\t\t\tint dy = 0;\n\t\t\tint codepoint = val;\n\t\t\tint font = fontid;\n\t\t\t\n\t\t\tif (icon > 0) {\n\t\t\t\tcodepoint = icon -1;\n\t\t\t\tfont = FONT_ICON;\n\t\t\t\t\n\t\t\t\tdy = - ( ctx.ascent ) * 256;\n\t\t\t}\n\t\t\t\n\t\t\tif (prim) {\n\t\t\t\tprim[n].pos.x = ctx.x * 256;\n\t\t\t\tprim[n].pos.y = ctx.y * 256 + dy;\n\t\t\t\tprim[n].pos.sr = 0;\n\t\t\t\tprim[n].pos.sprite = -material_id;\n\t\t\t}\n\t\t\t\n\t\t\tstruct font_glyph g, og;\n\t\t\tif (font_manager_glyph(mgr, font, codepoint, fontsize, &g, &og) == NULL) {\n\t\t\t\tadvance_position(pos, &ctx);\n\t\t\t\tif (ctx.x > ctx.line_width)\n\t\t\t\t\tctx.line_width = ctx.x;\n\t\t\t\tif (advance(&ctx, g.advance_x)) {\n\t\t\t\t\tif (newline(&ctx, prim, n, pos))\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tif (prim) {\n\t\t\t\t\t\tprim[n].pos.x = ctx.x * 256;\n\t\t\t\t\t\tprim[n].pos.y = ctx.y * 256 + dy;\n\t\t\t\t\t}\n\t\t\t\t\tadvance(&ctx, g.advance_x);\n\t\t\t\t}\n\t\t\t\tif (prim) {\n\t\t\t\t\tprim[n].u.text.header.sprite = -1;\n\t\t\t\t\tprim[n].u.text.codepoint = codepoint;\n\t\t\t\t\tprim[n].u.text.font = font;\n\t\t\t\t\tprim[n].u.text.size = fontsize;\n\t\t\t\t\tprim[n].u.text.color = ctx.color;\n\t\t\t\t}\n\t\t\t\t++n;\n\t\t\t}\n\t\t\t++i;\n\t\t}\n\t}\n\tif (ctx.x > ctx.line_width)\n\t\tctx.line_width = ctx.x;\n\tint height = ctx.y + ctx.decent - gap;\n\t\n\tif (pos && pos->n >= 0) {\n\t\tpos_n -= pos->n;\n\t\tpos->n = 0;\n\t\tadvance_position(pos, &ctx);\n\t}\n\t\n\tnewline(&ctx, prim, n, pos);\n\tint offy;\n\tint valign = ctx.alignment & VALIGNMENT_MASK;\n\tswitch (valign) {\n\tcase VALIGNMENT_CENTER:\n\t\toffy = (ctx.height - height) / 2 * 256;\n\t\tbreak;\n\tcase VALIGNMENT_BOTTOM:\n\t\toffy = (ctx.height - height) * 256;\n\t\tbreak;\n\tdefault:\n\t\toffy = 0;\n\t\tbreak;\n\t}\n\tif (prim != NULL) {\n\t\tif (offy != 0) {\n\t\t\tfor (i=0;i<n;i++) {\n\t\t\t\tprim[i].pos.y += offy;\n\t\t\t}\n\t\t}\n\t\tlua_pushexternalstring(L, buffer, n * sizeof(struct text_primitive), free_primitive, NULL);\n\t\tlua_pushinteger(L, height);\n\t\treturn 2;\n\t} else {\n\t\tpos->y += offy / 256 - ctx.ascent;\n\t\tpos->w = 2;\n\t\tpos->h = ctx.ascent + ctx.decent - gap;\n\t\tpos->n = pos_n;\n\t\tpos->decent = ctx.decent;\n\t\treturn 0;\n\t}\n}\n\nstatic int\nltext(lua_State *L) {\n\treturn ltext_(L, NULL);\n}\n\nstatic int\nltext_position(lua_State *L) {\n\tstruct position pos;\n\tpos.n = luaL_checkinteger(L, 2);\n\tpos.x = 0;\n\tpos.y = 0;\n\tpos.w = 0;\n\tpos.h = 0;\n\tlua_remove(L, 2);\n\tltext_(L, &pos);\n\tlua_pushinteger(L, pos.x);\n\tlua_pushinteger(L, pos.y);\n\tlua_pushinteger(L, pos.w);\n\tlua_pushinteger(L, pos.h);\n\tlua_pushinteger(L, pos.n);\n\tlua_pushinteger(L, pos.decent);\n\treturn 6;\n}\n\nstatic uint32_t\nparse_alignment(lua_State *L, int index) {\n\tconst char *alignment_string = lua_tostring(L, index);\n\tint i;\n\tchar c;\n\tuint32_t alignment = 0;\n\tfor (i=0;(c = alignment_string[i]);i++) {\n\t\tswitch(c) {\n\t\tcase 'l' :\n\t\tcase 'L' :\n\t\t\talignment |= ALIGNMENT_LEFT;\n\t\t\tbreak;\n\t\tcase 'r' :\n\t\tcase 'R' :\n\t\t\talignment |= ALIGNMENT_RIGHT;\n\t\t\tbreak;\n\t\tcase 'c' :\n\t\tcase 'C' :\n\t\t\talignment |= ALIGNMENT_CENTER;\n\t\t\tbreak;\n\t\tcase 't' :\n\t\tcase 'T' :\n\t\t\talignment |= VALIGNMENT_TOP;\n\t\t\tbreak;\n\t\tcase 'v' :\n\t\tcase 'V' :\n\t\t\talignment |= VALIGNMENT_CENTER;\n\t\t\tbreak;\n\t\tcase 'b' :\n\t\tcase 'B' :\n\t\t\talignment |= VALIGNMENT_BOTTOM;\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn alignment;\n}\n\nstatic int\nltext_block(lua_State *L) {\n\tif (material_id <= 0) {\n\t\treturn luaL_error(L, \"Text material is not registered\");\n\t}\n\tluaL_checktype(L, 1, LUA_TLIGHTUSERDATA);\n\tvoid * font_mgr = lua_touserdata(L, 1);\n\tint fontid = luaL_checkinteger(L, 2);\n\tint fontsize = luaL_optinteger(L, 3, DEFAULT_FONTSIZE);\n\tuint32_t color = luaL_optinteger(L, 4, 0xff000000);\n\tuint32_t alignment = 0;\n\tif (lua_type(L, 5) == LUA_TSTRING) {\n\t\talignment = parse_alignment(L, 5);\n\t}\n\tif (!(color & 0xff000000))\n\t\tcolor |= 0xff000000;\n\tlua_pushlightuserdata(L, font_mgr);\t// 1\n\tlua_pushinteger(L, fontid);\t// 2\n\tlua_pushinteger(L, fontsize);\t// 3\n\tlua_pushinteger(L, color);\t// 4\n\tlua_pushinteger(L, alignment);\t// 5\n\tlua_pushcclosure(L, ltext, 5);\n\tlua_pushlightuserdata(L, font_mgr);\t// 1\n\tlua_pushinteger(L, fontid);\t// 2\n\tlua_pushinteger(L, fontsize);\t// 3\n\tlua_pushinteger(L, color);\t// 4\n\tlua_pushinteger(L, alignment);\t// 5\n\tlua_pushcclosure(L, ltext_position, 5);\n\treturn 2;\n}\n\nint\nluaopen_material_text(lua_State *L) {\n\tluaL_checkversion(L);\n\tluaL_Reg l[] = {\n\t\t{ \"char\", NULL },\n\t\t{ \"set_material_id\", NULL },\n\t\t{ \"block\", ltext_block },\n\t\t{ \"normal\", lnew_material_text_normal },\n\t\t{ \"instance_size\", NULL },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\t\n\t// char()\n\tstruct text * t = lua_newuserdatauv(L, sizeof(*t), 1);\n\tmemset(t, 0, sizeof(*t));\n\tlua_pushvalue(L, -1);\n\tlua_pushcclosure(L, lset_material_id, 1);\n\tlua_setfield(L, -3, \"set_material_id\");\n\tlua_pushcclosure(L, lchar_for_batch, 1);\n\tlua_setfield(L, -2, \"char\");\n\t\n\tlua_pushinteger(L, sizeof(struct inst_object));\n\tlua_setfield(L, -2, \"instance_size\");\n\treturn 1;\n}\n"
  },
  {
    "path": "src/material_util.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n\n#include \"material_util.h\"\n\nvoid\nutil_ref_object(lua_State *L, void *ptr, int uv_index, const char *key, const char *luatype, int direct) {\n\tif (lua_getfield(L, 1, key) != LUA_TUSERDATA)\n\t\tluaL_error(L, \"Invalid key .%s\", key);\n\tvoid *obj = luaL_checkudata(L, -1, luatype);\n\tlua_pushvalue(L, -1);\n\t// ud, object, object\n\tlua_setiuservalue(L, -3, uv_index);\n\tif (!direct) {\n\t\tlua_pushlightuserdata(L, ptr);\n\t\tlua_call(L, 1, 0);\n\t} else {\n\t\tlua_pop(L, 1);\n\t\tvoid **ref = (void **)ptr;\n\t\t*ref = obj;\n\t}\n}\n\nvoid\nutil_submit_material(lua_State *L, int batch_n, void *mat, util_submit_func submit) {\n\tstruct draw_primitive *prim = lua_touserdata(L, 2);\n\tint prim_n = luaL_checkinteger(L, 3);\n\tint i = 0;\n\tfor (;;) {\n\t\tint n = prim_n - i;\n\t\tif (n > batch_n) {\n\t\t\tsubmit(L, mat, prim, batch_n);\n\t\t\ti += batch_n;\n\t\t\tprim += batch_n;\n\t\t} else {\n\t\t\tsubmit(L, mat, prim, n);\n\t\t\ti += batch_n;\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nsg_pipeline\nutil_make_pipeline(sg_pipeline_desc *desc, util_shader_desc_func func, const char *what, int blend) {\n\tsg_shader shd = sg_make_shader(func(sg_query_backend()));\n\tif (sg_query_shader_state(shd) != SG_RESOURCESTATE_VALID) {\n\t\tfprintf(stderr, \"Failed to create shader for %s!\\n\", what);\n\t}\n\tdesc->shader = shd;\n\tif (desc->primitive_type == 0) {\n\t\tdesc->primitive_type = SG_PRIMITIVETYPE_TRIANGLE_STRIP;\n\t}\n\tdesc->label = what;\n\tif (desc->layout.buffers[0].step_func == 0) {\n\t\tdesc->layout.buffers[0].step_func = SG_VERTEXSTEP_PER_INSTANCE;\n\t}\n\tif (blend) {\n\t\tdesc->colors[0].blend = (sg_blend_state) {\n\t\t\t.enabled = true,\n\t\t\t.src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA,\n\t\t\t.dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,\n\t\t\t.src_factor_alpha = SG_BLENDFACTOR_ONE,\n\t\t\t.dst_factor_alpha = SG_BLENDFACTOR_ZERO\n\t\t};\n\t}\n\tsg_pipeline pip = sg_make_pipeline(desc);\n\tif (sg_query_pipeline_state(pip) != SG_RESOURCESTATE_VALID) {\n\t\tfprintf(stderr, \"failed to create pipeline %s\\n\", what);\n\t}\n\treturn pip;\n}\n"
  },
  {
    "path": "src/material_util.h",
    "content": "#ifndef soluna_material_util_h\n#define soluna_material_util_h\n\n#include <lua.h>\n#include \"sokol/sokol_gfx.h\"\n#include \"batch.h\"\n\nvoid util_ref_object(lua_State *L, void *ptr, int uv_index, const char *key, const char *luatype, int direct);\n\ntypedef void (*util_submit_func)(lua_State *L, void *m_, struct draw_primitive *prim, int n);\nvoid util_submit_material(lua_State *L, int batch_n, void *mat, util_submit_func submit);\n\ntypedef const sg_shader_desc* (*util_shader_desc_func)(sg_backend backend);\nsg_pipeline util_make_pipeline(sg_pipeline_desc *desc, util_shader_desc_func func, const char *what, int blend);\n\n#endif\n"
  },
  {
    "path": "src/mutex.h",
    "content": "#ifndef __fontmutex_h_\n#define __fontmutex_h_\n\n#if defined(_WIN32)\n    #include <windows.h>\n    #define mutex_t SRWLOCK\n    #define mutex_init(m) InitializeSRWLock(&m)\n    #define mutex_acquire(m) AcquireSRWLockExclusive(&m)\n    #define mutex_release(m) ReleaseSRWLockExclusive(&m)\n#else\n    #include <pthread.h>\n    #define mutex_t pthread_mutex_t\n    #define mutex_init(m) pthread_mutex_init(&m, NULL)\n    #define mutex_acquire(m) pthread_mutex_lock(&m)\n    #define mutex_release(m) pthread_mutex_unlock(&m)\n#endif\n\n\n#endif\n"
  },
  {
    "path": "src/openlibs.c",
    "content": "#define linit_c\n#define LUA_LIB\n\n#include <stddef.h>\n#include \"lua.h\"\n#include \"lualib.h\"\n#include \"lauxlib.h\"\n\nvoid soluna_embed(lua_State* L);\n\nvoid\nsoluna_openlibs(lua_State *L) {\n\t// ignore env. vars.\n    lua_pushboolean(L, 1);\n    lua_setfield(L, LUA_REGISTRYINDEX, \"LUA_NOENV\");\n\tluaL_openlibs(L);\n    soluna_embed(L);\n}\n"
  },
  {
    "path": "src/openurl.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n\n#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)\n\n#include <windows.h>\n\nstatic void\nopen_url(lua_State *L, const char *url) {\n\tint n = MultiByteToWideChar(CP_UTF8, 0, url, -1, NULL, 0);\n\tif (n == 0)\n\t\tluaL_error(L, \"Invalid url string : %s\", url);\n\tvoid * buf = lua_newuserdatauv(L, n * sizeof(WCHAR), 0);\n\tMultiByteToWideChar(CP_UTF8, 0, url, -1, (WCHAR *)buf, n);\n\tShellExecuteW(NULL, L\"open\", (WCHAR *)buf, NULL, NULL, SW_SHOWNORMAL);\n}\n\n#elif defined(__EMSCRIPTEN__)\n#include <emscripten/emscripten.h>\n#if defined(__EMSCRIPTEN_PTHREADS__)\n#include <emscripten/threading.h>\n#endif\n\nextern void soluna_wasm_open_url(const char *url);\n\nstatic void\nsoluna_wasm_call_open_url(const char *url) {\n#if defined(__EMSCRIPTEN_PTHREADS__)\n  if (!emscripten_is_main_browser_thread()) {\n    emscripten_async_run_in_main_runtime_thread(EM_FUNC_SIG_VI, soluna_wasm_open_url, url);\n    return;\n  }\n#endif\n  soluna_wasm_open_url(url);\n}\n\nstatic void\nopen_url(lua_State *L, const char *url) {\n  soluna_wasm_call_open_url(url);\n}\n\n#elif defined(__APPLE__) || defined(__linux__)\n\n#include <unistd.h>\n#include <sys/wait.h>\n#include <stdlib.h>\n#include <string.h>\n\nstatic void\nopen_url(lua_State *L, const char *url) {\n  pid_t pid = fork();\n  if (pid < 0) {\n    luaL_error(L, \"fork() failed\");\n  } else if (pid == 0) {\n    // child\n#if defined(__APPLE__)\n    execl(\"/usr/bin/open\", \"open\", url, (char *)NULL);\n#else\n    execl(\"/usr/bin/xdg-open\", \"xdg-open\", url, (char *)NULL);\n#endif\n    // if execl return, it's error\n    _exit(127);\n  } else {\n    // parent\n    int status;\n    waitpid(pid, &status, 0);\n    if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {\n      luaL_error(L, \"failed to open url: %s\", url);\n    }\n  }\n}\n\n#else\n\nstatic void\nopen_url(lua_State *L, const char *url) {\n}\n\n#endif\n\nstatic int\nlurl_open(lua_State *L) {\n\tconst char * url = luaL_checkstring(L, 1);\n\topen_url(L, url);\n\treturn 0;\n}\n\nint\nluaopen_url(lua_State *L) {\n\tluaL_checkversion(L);\n\tluaL_Reg l[] = {\n\t\t{ \"open\", lurl_open },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\treturn 1;\n}\n"
  },
  {
    "path": "src/platform/linux/soluna_linux_ime.c",
    "content": "#include <X11/Xlib.h>\n#include <X11/Xutil.h>\n#include <X11/Xatom.h>\n#include <wchar.h>\n#include <uchar.h>\n#include <dlfcn.h>\n#include <stdlib.h>\n#include <string.h>\n#include <stdio.h>\n#include <locale.h>\n\n#include \"sokol/sokol_app.h\"\n\n#include \"ime_state.h\"\n#include \"ime_char_filter.h\"\n#include \"soluna_linux_ime.h\"\n\nvoid soluna_emit_char(uint32_t codepoint, uint32_t modifiers, bool repeat);\n\nstatic XIM g_soluna_linux_im = NULL;\nstatic XIC g_soluna_linux_ic = NULL;\nstatic bool g_soluna_linux_xim_failed = false;\nstatic bool g_soluna_linux_has_focus = false;\nstatic bool g_soluna_linux_locale_ready = false;\n\nstatic const int SOLUNA_LINUX_CHAR_QUEUE_CAP = 32;\nstatic uint32_t g_soluna_linux_expected_chars[32];\nstatic int g_soluna_linux_expected_count = 0;\nstatic uint32_t g_soluna_linux_ignore_chars[32];\nstatic int g_soluna_linux_ignore_count = 0;\n\nstatic inline struct soluna_ime_char_filter_state\nsoluna_linux_char_filter_state(void) {\n    return (struct soluna_ime_char_filter_state) {\n        .expected_chars = g_soluna_linux_expected_chars,\n        .expected_count = &g_soluna_linux_expected_count,\n        .ignore_chars = g_soluna_linux_ignore_chars,\n        .ignore_count = &g_soluna_linux_ignore_count,\n        .capacity = SOLUNA_LINUX_CHAR_QUEUE_CAP,\n    };\n}\n\nstatic void\nsoluna_linux_reset_char_queues(void) {\n    soluna_ime_char_filter_reset(soluna_linux_char_filter_state());\n}\n\nstatic Display *\nsoluna_linux_display(void) {\n    return (Display *)sapp_x11_get_display();\n}\n\nstatic Window\nsoluna_linux_window(void) {\n    return (Window)(uintptr_t)sapp_x11_get_window();\n}\n\nstatic void\nsoluna_linux_ensure_locale(void) {\n    if (g_soluna_linux_locale_ready) {\n        return;\n    }\n    if (!setlocale(LC_CTYPE, \"\")) {\n        fprintf(stderr, \"soluna: failed to set locale\\n\");\n    }\n    if (!XSupportsLocale()) {\n        fprintf(stderr, \"soluna: current locale is not supported by Xlib\\n\");\n    }\n    XSetLocaleModifiers(\"\");\n    g_soluna_linux_locale_ready = true;\n}\n\nstatic void\nsoluna_linux_set_spot(short x, short y) {\n    if (!g_soluna_linux_ic) {\n        return;\n    }\n    XVaNestedList preedit = XVaCreateNestedList(0,\n        XNSpotLocation, &(XPoint){ x, y },\n        NULL);\n    if (!preedit) {\n        return;\n    }\n    XSetICValues(g_soluna_linux_ic, XNPreeditAttributes, preedit, NULL);\n    XFree(preedit);\n}\n\nbool\nsoluna_linux_ensure_im(void) {\n    if (g_soluna_linux_ic) {\n        return true;\n    }\n    if (g_soluna_linux_xim_failed) {\n        return false;\n    }\n    Display *dpy = soluna_linux_display();\n    Window win = soluna_linux_window();\n    if (!dpy || !win) {\n        return false;\n    }\n    soluna_linux_ensure_locale();\n    XIM im = XOpenIM(dpy, NULL, NULL, NULL);\n    if (!im) {\n        g_soluna_linux_xim_failed = true;\n        return false;\n    }\n    XIC ic = XCreateIC(\n        im,\n        XNInputStyle, XIMPreeditNothing | XIMStatusNothing,\n        XNClientWindow, win,\n        XNFocusWindow, win,\n        NULL);\n    if (!ic) {\n        XCloseIM(im);\n        return false;\n    }\n    g_soluna_linux_im = im;\n    g_soluna_linux_ic = ic;\n    soluna_linux_set_spot(0, 0);\n    return true;\n}\n\nvoid\nsoluna_linux_update_spot(void) {\n    if (!g_soluna_ime_rect.valid) {\n        return;\n    }\n    if (!soluna_linux_ensure_im()) {\n        return;\n    }\n    float scale = sapp_dpi_scale();\n    if (scale <= 0.0f) {\n        scale = 1.0f;\n    }\n    float caret_x = g_soluna_ime_rect.x;\n    float caret_y = g_soluna_ime_rect.y + g_soluna_ime_rect.h;\n    if (caret_x < 0.0f) {\n        caret_x = 0.0f;\n    }\n    if (caret_y < 0.0f) {\n        caret_y = 0.0f;\n    }\n    short spot_x = (short)(caret_x * scale + 0.5f);\n    short spot_y = (short)(caret_y * scale + 0.5f);\n    soluna_linux_set_spot(spot_x, spot_y);\n}\n\nstatic void\nsoluna_linux_emit_utf8(const char *text, int len, uint32_t mods, bool repeat) {\n    if (!text || len <= 0) {\n        return;\n    }\n    mbstate_t state;\n    memset(&state, 0, sizeof(state));\n    const char *ptr = text;\n    const char *end = text + len;\n    bool first = true;\n    while (ptr < end) {\n        char32_t ch = 0;\n        size_t consumed = mbrtoc32(&ch, ptr, (size_t)(end - ptr), &state);\n        if (consumed == (size_t)-1 || consumed == (size_t)-2) {\n            memset(&state, 0, sizeof(state));\n            ++ptr;\n            continue;\n        }\n        if (consumed == 0) {\n            consumed = 1;\n        }\n        soluna_ime_char_filter_push_expected(soluna_linux_char_filter_state(), (uint32_t)ch);\n        soluna_emit_char((uint32_t)ch, mods, first ? repeat : false);\n        first = false;\n        ptr += consumed;\n    }\n}\n\nstatic bool\nsoluna_linux_handle_keypress(XKeyEvent *kev) {\n    if (!kev) {\n        return false;\n    }\n    Display *dpy = soluna_linux_display();\n    if (!dpy || kev->display != dpy || kev->window != soluna_linux_window()) {\n        return false;\n    }\n    if (!soluna_linux_ensure_im()) {\n        return false;\n    }\n    char local_buf[128];\n    char *buf = local_buf;\n    int cap = sizeof(local_buf);\n    KeySym ks = 0;\n    Status status = 0;\n    int len = Xutf8LookupString(g_soluna_linux_ic, kev, buf, cap, &ks, &status);\n    if (status == XBufferOverflow) {\n        buf = (char *)malloc((size_t)len);\n        if (!buf) {\n            return false;\n        }\n        cap = len;\n        len = Xutf8LookupString(g_soluna_linux_ic, kev, buf, cap, &ks, &status);\n    }\n    bool handled = false;\n    if (status == XLookupChars || status == XLookupBoth) {\n        uint32_t mods = 0;\n        if (kev->state & ShiftMask) mods |= SAPP_MODIFIER_SHIFT;\n        if (kev->state & ControlMask) mods |= SAPP_MODIFIER_CTRL;\n        if (kev->state & Mod1Mask) mods |= SAPP_MODIFIER_ALT;\n        bool repeat = (kev->type == KeyPress) && (kev->state & (1 << 14));\n        soluna_linux_emit_utf8(buf, len, mods, repeat);\n        handled = true;\n    }\n    if (buf != local_buf) {\n        free(buf);\n    }\n    return handled;\n}\n\nstatic bool\nsoluna_linux_filter_event(Display *dpy, XEvent *event) {\n    if (!event) {\n        return true;\n    }\n    if (XFilterEvent(event, None)) {\n        return true;\n    }\n    if (event->type == KeyPress) {\n        if (!g_soluna_ime_rect.valid) {\n            return false;\n        }\n        if (event->xkey.display != soluna_linux_display() || event->xkey.window != soluna_linux_window()) {\n            return false;\n        }\n        if (soluna_linux_handle_keypress(&event->xkey)) {\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic int (*soluna_linux_real_XNextEvent)(Display *, XEvent *) = NULL;\n\nstatic int\nsoluna_linux_XNextEvent(Display *display, XEvent *event) {\n    if (!soluna_linux_real_XNextEvent) {\n        soluna_linux_real_XNextEvent = (int (*)(Display *, XEvent *))dlsym(RTLD_NEXT, \"XNextEvent\");\n        if (!soluna_linux_real_XNextEvent) {\n            fprintf(stderr, \"soluna: failed to resolve XNextEvent\\n\");\n            abort();\n        }\n    }\n    int r = soluna_linux_real_XNextEvent(display, event);\n    if (r != 0) {\n        return r;\n    }\n    if (soluna_linux_filter_event(display, event)) {\n        event->type = 0;\n    }\n    return 0;\n}\n\nint\nXNextEvent(Display *display, XEvent *event) {\n    return soluna_linux_XNextEvent(display, event);\n}\n\nstatic void\nsoluna_linux_focus_reset(void) {\n    soluna_linux_reset_char_queues();\n}\n\nvoid\nsoluna_linux_focus_in(void) {\n    g_soluna_linux_xim_failed = false;\n    if (!soluna_linux_ensure_im()) {\n        return;\n    }\n    if (!g_soluna_linux_has_focus) {\n        XSetICFocus(g_soluna_linux_ic);\n        g_soluna_linux_has_focus = true;\n    }\n    soluna_linux_update_spot();\n}\n\nvoid\nsoluna_linux_focus_out(void) {\n    if (!g_soluna_linux_ic) {\n        return;\n    }\n    if (g_soluna_linux_has_focus) {\n        XUnsetICFocus(g_soluna_linux_ic);\n        g_soluna_linux_has_focus = false;\n    }\n    soluna_linux_focus_reset();\n}\n\nvoid\nsoluna_linux_shutdown_ime(void) {\n    if (g_soluna_linux_ic) {\n        XDestroyIC(g_soluna_linux_ic);\n        g_soluna_linux_ic = NULL;\n    }\n    if (g_soluna_linux_im) {\n        XCloseIM(g_soluna_linux_im);\n        g_soluna_linux_im = NULL;\n    }\n    g_soluna_linux_has_focus = false;\n    g_soluna_linux_xim_failed = false;\n    soluna_linux_focus_reset();\n}\n\nvoid\nsoluna_linux_on_rect_cleared(void) {\n    if (soluna_linux_ensure_im()) {\n        soluna_linux_set_spot(0, 0);\n    }\n    soluna_linux_focus_reset();\n}\n\nbool\nsoluna_linux_should_skip_event(const sapp_event *ev) {\n    if (!ev || ev->type != SAPP_EVENTTYPE_CHAR) {\n        return false;\n    }\n    return soluna_ime_char_filter_should_skip(soluna_linux_char_filter_state(), ev->char_code);\n}\n\nvoid\nsoluna_linux_handle_event(const sapp_event *ev) {\n    if (!ev) {\n        return;\n    }\n    switch (ev->type) {\n    case SAPP_EVENTTYPE_FOCUSED:\n        soluna_linux_focus_in();\n        if (g_soluna_ime_rect.valid) {\n            soluna_linux_update_spot();\n        }\n        break;\n    case SAPP_EVENTTYPE_UNFOCUSED:\n        soluna_linux_focus_out();\n        break;\n    case SAPP_EVENTTYPE_RESIZED:\n        if (g_soluna_ime_rect.valid) {\n            soluna_linux_update_spot();\n        }\n        break;\n    default:\n        break;\n    }\n}\n"
  },
  {
    "path": "src/platform/linux/soluna_linux_ime.h",
    "content": "#ifndef SOLUNA_LINUX_IME_H\n#define SOLUNA_LINUX_IME_H\n\n#include <stdbool.h>\n\nbool soluna_linux_ensure_im(void);\nvoid soluna_linux_on_rect_cleared(void);\nvoid soluna_linux_update_spot(void);\nvoid soluna_linux_focus_in(void);\nvoid soluna_linux_focus_out(void);\nvoid soluna_linux_shutdown_ime(void);\nbool soluna_linux_should_skip_event(const sapp_event *ev);\nvoid soluna_linux_handle_event(const sapp_event *ev);\n\n#endif /* SOLUNA_LINUX_IME_H */\n"
  },
  {
    "path": "src/platform/macos/soluna_macos_ime.h",
    "content": "#ifndef SOLUNA_MACOS_IME_H\n#define SOLUNA_MACOS_IME_H\n\n#include <stdbool.h>\n\nvoid soluna_macos_install_ime(void);\nvoid soluna_macos_hide_ime_label(void);\nvoid soluna_macos_apply_ime_rect(void);\nvoid soluna_macos_set_ime_font(const char *font_name, float height_px);\nbool soluna_macos_is_composition_active(void);\n\n#endif /* SOLUNA_MACOS_IME_H */\n"
  },
  {
    "path": "src/platform/macos/soluna_macos_ime.m",
    "content": "#import <Cocoa/Cocoa.h>\n#import <objc/runtime.h>\n\n#include <stdint.h>\n\n#include \"ime_state.h\"\n#include \"soluna_macos_ime.h\"\n#include \"sokol/sokol_app.h\"\n\n@interface _sapp_macos_view : NSView\n@end\n\nstatic void soluna_emit_nsstring(NSString *text);\n\n@interface SolunaIMETextView : NSTextView\n@end\n\n@implementation SolunaIMETextView\n- (BOOL)isOpaque {\n    return NO;\n}\n\n- (instancetype)initWithFrame:(NSRect)frameRect {\n    self = [super initWithFrame:frameRect];\n    if (self) {\n        [self setEditable:NO];\n        [self setSelectable:NO];\n        [self setRichText:NO];\n        [self setImportsGraphics:NO];\n        [self setAutomaticQuoteSubstitutionEnabled:NO];\n        [self setAutomaticDataDetectionEnabled:NO];\n        [self setAutomaticSpellingCorrectionEnabled:NO];\n        [self setDrawsBackground:NO];\n        [self setHorizontallyResizable:YES];\n        [self setVerticallyResizable:NO];\n        [self setTextContainerInset:NSMakeSize(0, 0)];\n        NSTextContainer *container = [self textContainer];\n        if (container) {\n            [container setLineFragmentPadding:0.0f];\n            [container setWidthTracksTextView:NO];\n            [container setContainerSize:NSMakeSize(CGFLOAT_MAX, CGFLOAT_MAX)];\n        }\n        [self setHidden:YES];\n    }\n    return self;\n}\n\n- (void)doCommandBySelector:(SEL)selector {\n    (void)selector;\n}\n@end\n\n// Forward declarations implemented in entry.c\nvoid soluna_emit_char(uint32_t codepoint, uint32_t modifiers, bool repeat);\n\nstatic bool g_soluna_macos_composition = false;\nstatic SolunaIMETextView *g_soluna_ime_label = nil;\nstatic NSString *g_soluna_macos_ime_font_name = nil;\nstatic CGFloat g_soluna_macos_ime_font_size = 14.0f;\nstatic SolunaIMETextView *soluna_macos_ensure_ime_label(NSView *view);\nstatic const void *const kSolunaMarkedTextKey = &kSolunaMarkedTextKey;\nstatic const void *const kSolunaSelectedRangeKey = &kSolunaSelectedRangeKey;\nstatic const void *const kSolunaConsumedFlagKey = &kSolunaConsumedFlagKey;\n\nstatic NSFont *\nsoluna_macos_current_ime_font(void) {\n    CGFloat size = g_soluna_macos_ime_font_size > 0.0f ? g_soluna_macos_ime_font_size : 14.0f;\n    NSFont *font = nil;\n    if (g_soluna_macos_ime_font_name) {\n        font = [NSFont fontWithName:g_soluna_macos_ime_font_name size:size];\n    }\n    if (font == nil) {\n        font = [NSFont systemFontOfSize:size];\n    }\n    return font;\n}\n\nstatic NSColor *\nsoluna_macos_current_text_color(void) {\n    if (g_soluna_ime_rect.text_color == 0) {\n        return [NSColor textColor];\n    }\n    uint32_t c = g_soluna_ime_rect.text_color;\n    CGFloat a = ((c >> 24) & 0xff) / 255.0f;\n    CGFloat r = ((c >> 16) & 0xff) / 255.0f;\n    CGFloat g = ((c >> 8) & 0xff) / 255.0f;\n    CGFloat b = (c & 0xff) / 255.0f;\n    return [NSColor colorWithRed:r green:g blue:b alpha:a];\n}\n\nstatic void\nsoluna_macos_apply_ime_font(void) {\n    if (g_soluna_ime_label) {\n        NSFont *font = soluna_macos_current_ime_font();\n        if (font) {\n            [g_soluna_ime_label setFont:font];\n        }\n    }\n}\n\nvoid\nsoluna_macos_set_ime_font(const char *font_name, float height_px) {\n    if (g_soluna_macos_ime_font_name) {\n        [g_soluna_macos_ime_font_name release];\n        g_soluna_macos_ime_font_name = nil;\n    }\n    if (font_name && font_name[0]) {\n        NSString *converted = [[NSString alloc] initWithUTF8String:font_name];\n        if (converted) {\n            g_soluna_macos_ime_font_name = converted;\n        } else {\n            [converted release];\n        }\n    }\n    if (height_px > 0.0f) {\n        g_soluna_macos_ime_font_size = (CGFloat)height_px;\n    } else {\n        g_soluna_macos_ime_font_size = 0.0f;\n    }\n    soluna_macos_apply_ime_font();\n}\n\nstatic NSRect\nsoluna_current_caret_local_rect(NSView *view) {\n    NSRect caret = NSMakeRect(0, 0, 1, 1);\n    if (g_soluna_ime_rect.valid) {\n        CGFloat dpi_scale = sapp_dpi_scale();\n        if (dpi_scale <= 0.0f) {\n            dpi_scale = 1.0f;\n        }\n        CGFloat logical_height = (CGFloat)sapp_height() / dpi_scale;\n        CGFloat caret_y = logical_height - (g_soluna_ime_rect.y + g_soluna_ime_rect.h);\n        caret = NSMakeRect(g_soluna_ime_rect.x, caret_y, g_soluna_ime_rect.w, g_soluna_ime_rect.h);\n    }\n    return caret;\n}\n\nstatic void\nsoluna_macos_position_ime_input_view(NSView *view) {\n    SolunaIMETextView *imeView = soluna_macos_ensure_ime_label(view);\n    if (!imeView || !g_soluna_ime_rect.valid) {\n        return;\n    }\n    NSRect caret = soluna_current_caret_local_rect(view);\n    NSRect bounds = view.bounds;\n    NSFont *font = soluna_macos_current_ime_font();\n    CGFloat ascender = 0.0f;\n    CGFloat descender = 0.0f;\n    CGFloat leading = 0.0f;\n    if (font) {\n        ascender = MAX(font.ascender, 0.0f);\n        descender = MIN(font.descender, 0.0f);\n        leading = MAX(font.leading, 0.0f);\n    }\n    CGFloat lineHeight = ascender - descender + leading;\n    if (lineHeight <= 0.0f) {\n        lineHeight = g_soluna_macos_ime_font_size > 0.0f ? g_soluna_macos_ime_font_size : 14.0f;\n    }\n    lineHeight = MAX(lineHeight, MAX(caret.size.height, 1.0f));\n    CGFloat baselineY = caret.origin.y + MAX(caret.size.height, 1.0f) * 0.5f;\n    CGFloat frameX = caret.origin.x;\n    CGFloat frameY = baselineY - ascender;\n    CGFloat frameW = MAX(1.0f, NSMaxX(bounds) - frameX);\n    CGFloat frameH = lineHeight;\n    NSRect frame = NSMakeRect(frameX, frameY, frameW, frameH);\n    CGFloat maxOriginX = NSMaxX(bounds) - 1.0f;\n    if (maxOriginX < bounds.origin.x) {\n        maxOriginX = bounds.origin.x;\n    }\n    if (frame.origin.x < bounds.origin.x) frame.origin.x = bounds.origin.x;\n    if (frame.origin.x > maxOriginX) frame.origin.x = maxOriginX;\n    if (frame.origin.y < bounds.origin.y) frame.origin.y = bounds.origin.y;\n    if (NSMaxX(frame) > NSMaxX(bounds)) frame.size.width = MAX(1.0f, NSMaxX(bounds) - frame.origin.x);\n    if (NSMaxY(frame) > NSMaxY(bounds)) frame.origin.y = NSMaxY(bounds) - frame.size.height;\n    [imeView setFrame:NSIntegralRect(frame)];\n    [imeView setHidden:NO];\n}\n\nstatic SolunaIMETextView *\nsoluna_macos_ensure_ime_label(NSView *view) {\n    if (g_soluna_ime_label == nil) {\n        g_soluna_ime_label = [[SolunaIMETextView alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)];\n        [g_soluna_ime_label setHidden:YES];\n        [g_soluna_ime_label setTranslatesAutoresizingMaskIntoConstraints:YES];\n    }\n    if (g_soluna_ime_label.superview != view) {\n        [g_soluna_ime_label removeFromSuperview];\n        if (view) {\n            [view addSubview:g_soluna_ime_label];\n        }\n    }\n    soluna_macos_apply_ime_font();\n    return g_soluna_ime_label;\n}\n\nvoid\nsoluna_macos_hide_ime_label(void) {\n    if (g_soluna_ime_label) {\n        [g_soluna_ime_label setString:@\"\"];\n        [g_soluna_ime_label setHidden:YES];\n    }\n}\n\nstatic uint32_t\nsoluna_modifiers_from_event(NSEvent *event) {\n    NSEventModifierFlags flags = event ? event.modifierFlags : NSEvent.modifierFlags;\n    uint32_t mods = 0;\n    if (flags & NSEventModifierFlagShift) {\n        mods |= SAPP_MODIFIER_SHIFT;\n    }\n    if (flags & NSEventModifierFlagControl) {\n        mods |= SAPP_MODIFIER_CTRL;\n    }\n    if (flags & NSEventModifierFlagOption) {\n        mods |= SAPP_MODIFIER_ALT;\n    }\n    if (flags & NSEventModifierFlagCommand) {\n        mods |= SAPP_MODIFIER_SUPER;\n    }\n    return mods;\n}\n\nstatic uint32_t\nsoluna_utf32_from_substring(NSString *substr) {\n    if (substr == nil || substr.length == 0) {\n        return 0;\n    }\n    unichar buffer[2] = {0};\n    NSUInteger len = substr.length;\n    [substr getCharacters:buffer range:NSMakeRange(0, len)];\n    if (len >= 2 && buffer[0] >= 0xD800 && buffer[0] <= 0xDBFF && buffer[1] >= 0xDC00 && buffer[1] <= 0xDFFF) {\n        uint32_t high = buffer[0] - 0xD800;\n        uint32_t low = buffer[1] - 0xDC00;\n        return (high << 10) + low + 0x10000;\n    }\n    return buffer[0];\n}\n\nstatic void\nsoluna_emit_nsstring(NSString *text) {\n    if (text == nil || text.length == 0) {\n        return;\n    }\n    uint32_t mods = soluna_modifiers_from_event([NSApp currentEvent]);\n    [text enumerateSubstringsInRange:NSMakeRange(0, text.length)\n        options:NSStringEnumerationByComposedCharacterSequences\n        usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) {\n            (void)substringRange;\n            (void)enclosingRange;\n            (void)stop;\n            uint32_t codepoint = soluna_utf32_from_substring(substring);\n            if (codepoint != 0) {\n                soluna_emit_char(codepoint, mods, false);\n            }\n        }];\n}\n\nstatic NSString *\nsoluna_plain_string(id string) {\n    if ([string isKindOfClass:[NSAttributedString class]]) {\n        return [(NSAttributedString *)string string];\n    }\n    if ([string isKindOfClass:[NSString class]]) {\n        return (NSString *)string;\n    }\n    return [string description];\n}\n\nstatic void\nsoluna_store_marked_text(NSView *view, NSString *text, NSRange selected_range) {\n    if (text != nil && text.length > 0) {\n        objc_setAssociatedObject(view, kSolunaMarkedTextKey, text, OBJC_ASSOCIATION_COPY_NONATOMIC);\n        NSValue *value = [NSValue valueWithRange:selected_range];\n        objc_setAssociatedObject(view, kSolunaSelectedRangeKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);\n    } else {\n        objc_setAssociatedObject(view, kSolunaMarkedTextKey, nil, OBJC_ASSOCIATION_ASSIGN);\n        objc_setAssociatedObject(view, kSolunaSelectedRangeKey, nil, OBJC_ASSOCIATION_ASSIGN);\n    }\n}\n\nstatic NSString *\nsoluna_current_marked_text(NSView *view) {\n    return objc_getAssociatedObject(view, kSolunaMarkedTextKey);\n}\n\nstatic bool\nsoluna_view_has_marked_text(NSView *view) {\n    NSString *text = soluna_current_marked_text(view);\n    return text != nil && text.length > 0;\n}\n\nstatic NSRange\nsoluna_current_selected_range(NSView *view) {\n    NSValue *value = objc_getAssociatedObject(view, kSolunaSelectedRangeKey);\n    if (value == nil) {\n        return NSMakeRange(NSNotFound, 0);\n    }\n    return [value rangeValue];\n}\n\nstatic void\nsoluna_set_event_consumed(NSView *view, bool consumed) {\n    if (consumed) {\n        objc_setAssociatedObject(view, kSolunaConsumedFlagKey, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC);\n    } else {\n        objc_setAssociatedObject(view, kSolunaConsumedFlagKey, nil, OBJC_ASSOCIATION_ASSIGN);\n    }\n}\n\nstatic bool\nsoluna_event_consumed(NSView *view) {\n    NSNumber *flag = objc_getAssociatedObject(view, kSolunaConsumedFlagKey);\n    return flag && [flag boolValue];\n}\n\nstatic void\nsoluna_update_ime_label(NSView *view, id markedText, NSRange selectedRange) {\n    NSString *plain = soluna_plain_string(markedText);\n    if (!g_soluna_ime_rect.valid || plain.length == 0) {\n        soluna_macos_hide_ime_label();\n        return;\n    }\n    SolunaIMETextView *imeView = soluna_macos_ensure_ime_label(view);\n    if (!imeView) {\n        return;\n    }\n    soluna_macos_position_ime_input_view(view);\n    NSMutableAttributedString *attr = nil;\n    if ([markedText isKindOfClass:[NSAttributedString class]]) {\n        attr = [[(NSAttributedString *)markedText mutableCopy] autorelease];\n    } else {\n        attr = [[[NSMutableAttributedString alloc] initWithString:plain] autorelease];\n    }\n    if (attr.length > 0) {\n        NSRange full = NSMakeRange(0, attr.length);\n        NSFont *font = soluna_macos_current_ime_font();\n        if (font) {\n            [attr addAttribute:NSFontAttributeName value:font range:full];\n        }\n        [attr addAttribute:NSForegroundColorAttributeName value:soluna_macos_current_text_color() range:full];\n        if ([attr attribute:NSUnderlineStyleAttributeName atIndex:0 effectiveRange:NULL] == nil) {\n            [attr addAttribute:NSUnderlineStyleAttributeName value:@(NSUnderlineStyleSingle) range:full];\n        }\n    }\n    [[imeView textStorage] setAttributedString:attr];\n    if (selectedRange.location != NSNotFound && NSMaxRange(selectedRange) <= attr.length) {\n        [imeView setSelectedRange:selectedRange];\n    } else {\n        [imeView setSelectedRange:NSMakeRange(attr.length, 0)];\n    }\n    [imeView setHidden:NO];\n}\n\nstatic NSRect\nsoluna_current_caret_screen_rect(NSView *view) {\n    NSRect caret = soluna_current_caret_local_rect(view);\n    caret = [view convertRect:caret toView:nil];\n    if (view.window) {\n        caret = [view.window convertRectToScreen:caret];\n    }\n    return caret;\n}\n\n@interface _sapp_macos_view (SolunaIME) <NSTextInputClient>\n- (void)soluna_keyDown:(NSEvent *)event;\n@end\n\n@implementation _sapp_macos_view (SolunaIME)\n\n- (void)soluna_keyDown:(NSEvent *)event {\n    bool wasComposing = g_soluna_macos_composition || soluna_view_has_marked_text(self);\n    if (g_soluna_ime_rect.valid && wasComposing) {\n        soluna_macos_position_ime_input_view(self);\n    }\n    soluna_set_event_consumed(self, false);\n    BOOL handled = [[self inputContext] handleEvent:event];\n    bool consumed = soluna_event_consumed(self);\n    bool hasMarked = soluna_view_has_marked_text(self);\n    if (handled && (consumed || hasMarked)) {\n        g_soluna_macos_composition = true;\n        return;\n    }\n    if (g_soluna_macos_composition && hasMarked) {\n        return;\n    }\n    g_soluna_macos_composition = false;\n    [self soluna_keyDown:event];\n}\n\n- (void)insertText:(id)string replacementRange:(NSRange)replacementRange {\n    (void)replacementRange;\n    NSString *plain = soluna_plain_string(string);\n    bool commit = (plain.length > 0) && g_soluna_macos_composition;\n    soluna_store_marked_text(self, nil, NSMakeRange(NSNotFound, 0));\n    if (commit) {\n        soluna_emit_nsstring(plain);\n    }\n    soluna_set_event_consumed(self, commit);\n    g_soluna_macos_composition = false;\n    soluna_macos_hide_ime_label();\n}\n\n- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange {\n    (void)replacementRange;\n    NSString *plain = soluna_plain_string(string);\n    soluna_store_marked_text(self, plain, selectedRange);\n    g_soluna_macos_composition = true;\n    soluna_update_ime_label(self, string, selectedRange);\n    soluna_set_event_consumed(self, true);\n}\n\n- (void)unmarkText {\n    soluna_store_marked_text(self, nil, NSMakeRange(NSNotFound, 0));\n    g_soluna_macos_composition = false;\n    soluna_macos_hide_ime_label();\n}\n\n- (NSRange)selectedRange {\n    NSRange range = soluna_current_selected_range(self);\n    if (range.location == NSNotFound) {\n        return NSMakeRange(0, 0);\n    }\n    return range;\n}\n\n- (NSRange)markedRange {\n    NSString *text = soluna_current_marked_text(self);\n    if (text != nil && text.length > 0) {\n        return NSMakeRange(0, text.length);\n    }\n    return NSMakeRange(NSNotFound, 0);\n}\n\n- (BOOL)hasMarkedText {\n    return soluna_view_has_marked_text(self);\n}\n\n- (NSArray<NSAttributedStringKey> *)validAttributesForMarkedText {\n    return @[];\n}\n\n- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange {\n    NSString *text = soluna_current_marked_text(self);\n    if (text == nil || range.location == NSNotFound) {\n        return nil;\n    }\n    NSUInteger end = range.location + range.length;\n    if (end > text.length) {\n        return nil;\n    }\n    NSString *substr = [text substringWithRange:range];\n    if (actualRange) {\n        *actualRange = range;\n    }\n    return [[[NSAttributedString alloc] initWithString:substr] autorelease];\n}\n\n- (NSUInteger)characterIndexForPoint:(NSPoint)point {\n    (void)point;\n    return 0;\n}\n\n- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange {\n    if (actualRange) {\n        *actualRange = range;\n    }\n    return soluna_current_caret_screen_rect(self);\n}\n\n- (void)doCommandBySelector:(SEL)selector {\n    id nextResponder = [self nextResponder];\n    if ([nextResponder respondsToSelector:@selector(doCommandBySelector:)]) {\n        [nextResponder doCommandBySelector:selector];\n    }\n}\n\n- (BOOL)acceptsFirstResponder {\n    return YES;\n}\n\n@end\n\nvoid\nsoluna_macos_install_ime(void) {\n    static bool installed = false;\n    if (installed) {\n        return;\n    }\n    Class viewCls = NSClassFromString(@\"_sapp_macos_view\");\n    if (!viewCls) {\n        return;\n    }\n    Method original = class_getInstanceMethod(viewCls, @selector(keyDown:));\n    Method replacement = class_getInstanceMethod(viewCls, @selector(soluna_keyDown:));\n    if (original && replacement) {\n        method_exchangeImplementations(original, replacement);\n    }\n    installed = true;\n}\n\nvoid\nsoluna_macos_apply_ime_rect(void) {\n    if (!g_soluna_ime_rect.valid) {\n        soluna_macos_hide_ime_label();\n        return;\n    }\n    if (g_soluna_ime_label) {\n        NSView *view = [g_soluna_ime_label superview];\n        if (view) {\n            NSString *text = soluna_current_marked_text(view);\n            if (text && text.length > 0) {\n                soluna_update_ime_label(view, text, soluna_current_selected_range(view));\n            } else {\n                soluna_macos_position_ime_input_view(view);\n            }\n        }\n    }\n}\n\nbool\nsoluna_macos_is_composition_active(void) {\n    return g_soluna_macos_composition;\n}\n"
  },
  {
    "path": "src/platform/wasm/soluna_ime.js",
    "content": "mergeInto(LibraryManager.library, {\n  soluna_wasm_setup_ime__deps: ['$withStackSave', '$lengthBytesUTF8', '$stringToUTF8', '$stackAlloc'],\n  soluna_wasm_setup_ime__sig: 'v',\n  soluna_wasm_setup_ime: function () {\n    if (Module.solunaIme) {\n      return;\n    }\n    if (typeof document === 'undefined' || !document.body) {\n      return;\n    }\n    var globalScope = (typeof window !== 'undefined') ? window : self;\n\n    var ta = document.createElement('textarea');\n    ta.setAttribute('autocapitalize', 'off');\n    ta.setAttribute('autocomplete', 'off');\n    ta.setAttribute('autocorrect', 'off');\n    ta.setAttribute('spellcheck', 'false');\n    ta.setAttribute('tabindex', '-1');\n    ta.style.position = 'absolute';\n    ta.style.opacity = '0';\n    ta.style.pointerEvents = 'none';\n    ta.style.zIndex = '2147483647';\n    ta.style.resize = 'none';\n    ta.style.overflow = 'hidden';\n    ta.style.border = '0';\n    ta.style.margin = '0';\n    ta.style.padding = '0';\n    ta.style.background = 'transparent';\n    ta.style.color = '#000';\n    ta.style.whiteSpace = 'pre';\n    ta.style.width = '1px';\n    ta.style.height = '1px';\n    ta.style.left = '-10000px';\n    ta.style.top = '-10000px';\n    ta.style.display = 'none';\n    document.body.appendChild(ta);\n\n    var label = document.createElement('div');\n    label.setAttribute('aria-hidden', 'true');\n    label.style.position = 'absolute';\n    label.style.pointerEvents = 'none';\n    label.style.zIndex = '2147483647';\n    label.style.whiteSpace = 'pre';\n    label.style.margin = '0';\n    label.style.padding = '0';\n    label.style.border = '0';\n    label.style.background = 'transparent';\n    label.style.display = 'none';\n    label.style.left = '-10000px';\n    label.style.top = '-10000px';\n    label.style.font = '16px sans-serif';\n    label.style.color = '#000';\n    document.body.appendChild(label);\n\n    var callSetComposing = function (flag) {\n      if (Module._soluna_wasm_set_composing) {\n        Module._soluna_wasm_set_composing(flag | 0);\n      }\n    };\n    Module.solunaSetComposing = callSetComposing;\n\n    var makeLatestTaskScheduler = function (run) {\n      var token = 0;\n      var schedule = function () {\n        var current = ++token;\n        setTimeout(function () {\n          if (current !== token) {\n            return;\n          }\n          run();\n        }, 0);\n      };\n      schedule.cancel = function () {\n        ++token;\n      };\n      return schedule;\n    };\n\n    var state = {\n      node: ta,\n      preedit: label,\n      preeditText: '',\n      active: false,\n      composing: false,\n      expectNextInput: false,\n      suppressNextInput: false,\n      mods: 0,\n      rect: { x: 0, y: 0, w: 1, h: 1 },\n      customFont: null,\n      customFontSize: 0,\n      customTextColor: null,\n      metricCanvas: null,\n      metricContext: null,\n    };\n\n    state.focusNode = function () {\n      var el = state.node;\n      if (!el) {\n        return;\n      }\n      try {\n        el.focus({ preventScroll: true });\n      } catch (err) {\n        el.focus();\n      }\n    };\n\n    state.queueFocus = makeLatestTaskScheduler(function () {\n      var el = state.node;\n      if (!el || !state.active) {\n        return;\n      }\n      el.style.display = 'block';\n      state.focusNode();\n    });\n\n    state.queueBlur = makeLatestTaskScheduler(function () {\n      var el = state.node;\n      if (!el) {\n        return;\n      }\n      el.blur();\n    });\n\n    state.resolveCanvas = function () {\n      if (Module.canvas) {\n        return Module.canvas;\n      }\n      if (typeof document === 'undefined') {\n        return null;\n      }\n      var selector = Module.solunaCanvasSelector || '#canvas';\n      var canvas = null;\n      try {\n        canvas = document.querySelector(selector);\n      } catch (err) {\n        canvas = null;\n      }\n      if (!canvas) {\n        canvas = document.querySelector('canvas');\n      }\n      return canvas || null;\n    };\n\n    state.updateModsFromEvent = function (ev) {\n      var mods = 0;\n      if (ev && ev.shiftKey) { mods |= 1; }\n      if (ev && ev.ctrlKey) { mods |= 2; }\n      if (ev && ev.altKey) { mods |= 4; }\n      if (ev && ev.metaKey) { mods |= 8; }\n      state.mods = mods;\n    };\n\n    state.applyFontOverride = function () {\n      var textEl = state.node;\n      var labelEl = state.preedit;\n      if (!textEl) {\n        return;\n      }\n      if (state.customFont && state.customFont.length > 0) {\n        textEl.style.fontFamily = state.customFont;\n        if (labelEl) {\n          labelEl.style.fontFamily = state.customFont;\n        }\n      } else {\n        textEl.style.fontFamily = '';\n      }\n      if (state.customFontSize > 0) {\n        var sizePx = state.customFontSize + 'px';\n        textEl.style.fontSize = sizePx;\n        if (labelEl) {\n          labelEl.style.fontSize = sizePx;\n        }\n      } else {\n        textEl.style.fontSize = '';\n      }\n    };\n\n    state.applyColorOverride = function () {\n      var textEl = state.node;\n      var labelEl = state.preedit;\n      if (!textEl) {\n        return;\n      }\n      if (state.customTextColor && state.customTextColor.length > 0) {\n        textEl.style.color = state.customTextColor;\n        if (labelEl) {\n          labelEl.style.color = state.customTextColor;\n        }\n      } else {\n        textEl.style.color = '';\n        if (labelEl) {\n          labelEl.style.color = '';\n        }\n      }\n    };\n\n    state.refreshColorFromContext = function () {\n      var canvas = state.resolveCanvas ? state.resolveCanvas() : null;\n      if (!state.customTextColor && canvas) {\n        state.syncStylesFromCanvas(canvas, state.rect ? state.rect.h : 0);\n        return;\n      }\n      state.applyColorOverride();\n    };\n\n    state.commitText = function (text) {\n      if (!text || text.length === 0) {\n        return;\n      }\n      var mods = state.mods;\n      withStackSave(function () {\n        var len = lengthBytesUTF8(text) + 1;\n        var ptr = stackAlloc(len);\n        stringToUTF8(text, ptr, len);\n        if (Module._soluna_wasm_ime_commit) {\n          Module._soluna_wasm_ime_commit(ptr, mods);\n        }\n      });\n    };\n\n    state.syncStylesFromCanvas = function (canvas, height) {\n      var textEl = state.node;\n      var labelEl = state.preedit;\n      if (canvas && typeof window !== 'undefined' && window.getComputedStyle) {\n        var computed = null;\n        try {\n          computed = window.getComputedStyle(canvas);\n        } catch (err) {\n          computed = null;\n        }\n        if (computed) {\n          if (textEl && computed.color) {\n            textEl.style.color = computed.color;\n          }\n          if (labelEl && computed.color) {\n            labelEl.style.color = computed.color;\n          }\n          if (computed.font && computed.font.length > 0) {\n            if (labelEl) {\n              labelEl.style.font = computed.font;\n            }\n          } else {\n            if (computed.fontSize) {\n              if (labelEl) {\n                labelEl.style.fontSize = computed.fontSize;\n              }\n            }\n            if (computed.fontFamily) {\n              if (labelEl) {\n                labelEl.style.fontFamily = computed.fontFamily;\n              }\n            }\n            if (computed.fontWeight) {\n              if (labelEl) {\n                labelEl.style.fontWeight = computed.fontWeight;\n              }\n            }\n          }\n        }\n      }\n      if (labelEl && Number.isFinite(height) && height > 0) {\n        labelEl.style.lineHeight = height + 'px';\n      }\n      state.applyFontOverride();\n      if (state.customTextColor && state.customTextColor.length > 0) {\n        state.applyColorOverride();\n      }\n    };\n\n    state.hidePreedit = function () {\n      state.preeditText = '';\n      var labelEl = state.preedit;\n      if (labelEl) {\n        labelEl.textContent = '';\n        labelEl.style.display = 'none';\n      }\n    };\n\n    state.parseCssPixel = function (value) {\n      if (!value || typeof value !== 'string') {\n        return 0;\n      }\n      var n = parseFloat(value);\n      return Number.isFinite(n) ? n : 0;\n    };\n\n    state.resolvePreeditAscent = function (labelEl, caretHeight) {\n      var lineHeight = caretHeight > 0 ? caretHeight : 16;\n      var fontSize = lineHeight;\n      var fontSpec = '';\n      if (typeof window !== 'undefined' && window.getComputedStyle) {\n        var computed = null;\n        try {\n          computed = window.getComputedStyle(labelEl);\n        } catch (err) {\n          computed = null;\n        }\n        if (computed) {\n          var parsedLineHeight = state.parseCssPixel(computed.lineHeight);\n          if (parsedLineHeight > 0) {\n            lineHeight = parsedLineHeight;\n          }\n          var parsedFontSize = state.parseCssPixel(computed.fontSize);\n          if (parsedFontSize > 0) {\n            fontSize = parsedFontSize;\n          }\n          if (computed.font && computed.font.length > 0) {\n            fontSpec = computed.font;\n          }\n        }\n      }\n      if (!fontSpec || fontSpec.length === 0) {\n        fontSpec = labelEl.style.font || (fontSize + 'px sans-serif');\n      }\n      var ascent = 0;\n      var descent = 0;\n      if (typeof document !== 'undefined') {\n        if (!state.metricCanvas) {\n          state.metricCanvas = document.createElement('canvas');\n        }\n        if (!state.metricContext && state.metricCanvas) {\n          state.metricContext = state.metricCanvas.getContext('2d');\n        }\n      }\n      if (state.metricContext) {\n        try {\n          state.metricContext.font = fontSpec;\n          var metrics = state.metricContext.measureText('Mg');\n          if (metrics) {\n            if (Number.isFinite(metrics.actualBoundingBoxAscent) && metrics.actualBoundingBoxAscent > 0) {\n              ascent = metrics.actualBoundingBoxAscent;\n            }\n            if (Number.isFinite(metrics.actualBoundingBoxDescent) && metrics.actualBoundingBoxDescent > 0) {\n              descent = metrics.actualBoundingBoxDescent;\n            }\n          }\n        } catch (err) {\n          ascent = 0;\n          descent = 0;\n        }\n      }\n      if (ascent <= 0) {\n        ascent = fontSize * 0.8;\n      }\n      if (descent <= 0) {\n        descent = Math.max(fontSize - ascent, fontSize * 0.2);\n      }\n      var contentHeight = ascent + descent;\n      var leading = lineHeight - contentHeight;\n      if (!Number.isFinite(leading) || leading < 0) {\n        leading = 0;\n      }\n      return ascent + leading * 0.5;\n    };\n\n    state.positionPreedit = function () {\n      var labelEl = state.preedit;\n      if (!labelEl || labelEl.style.display === 'none') {\n        return;\n      }\n      var canvas = state.resolveCanvas();\n      if (!canvas) {\n        return;\n      }\n      var rect = canvas.getBoundingClientRect();\n      var scrollX = (typeof window !== 'undefined' && typeof window.scrollX === 'number') ? window.scrollX : 0;\n      var scrollY = (typeof window !== 'undefined' && typeof window.scrollY === 'number') ? window.scrollY : 0;\n      var canvasLeft = rect.left + scrollX;\n      var canvasTop = rect.top + scrollY;\n      var caretX = canvasLeft + state.rect.x;\n      var caretY = canvasTop + state.rect.y;\n      var caretWidth = state.rect.w;\n      var caretHeight = state.rect.h;\n      if (!Number.isFinite(caretWidth) || caretWidth <= 0) {\n        caretWidth = 1;\n      }\n      if (!Number.isFinite(caretHeight) || caretHeight <= 0) {\n        caretHeight = 16;\n      }\n      var labelWidth = labelEl.offsetWidth;\n      var labelHeight = labelEl.offsetHeight;\n      if (labelWidth <= 0) {\n        labelWidth = caretWidth;\n      }\n      if (labelHeight <= 0) {\n        labelHeight = caretHeight;\n      }\n      var canvasRight = canvasLeft + rect.width;\n      var canvasBottom = canvasTop + rect.height;\n      var left = caretX;\n      if (left + labelWidth > canvasRight) {\n        left = canvasRight - labelWidth;\n      }\n      if (left < canvasLeft) {\n        left = canvasLeft;\n      }\n      var baseline = caretY + caretHeight;\n      var ascent = state.resolvePreeditAscent(labelEl, caretHeight);\n      if (!Number.isFinite(ascent) || ascent <= 0) {\n        ascent = labelHeight;\n      }\n      var top = baseline - ascent;\n      if (top < canvasTop) {\n        top = caretY + caretHeight;\n        if (top + labelHeight > canvasBottom) {\n          top = Math.max(canvasTop, Math.min(canvasBottom - labelHeight, baseline - ascent));\n        }\n      }\n      labelEl.style.left = Math.round(left) + 'px';\n      labelEl.style.top = Math.round(top) + 'px';\n    };\n\n    state.setPreeditText = function (text) {\n      var labelEl = state.preedit;\n      if (!labelEl) {\n        return;\n      }\n      state.preeditText = text || '';\n      if (!state.preeditText) {\n        state.hidePreedit();\n        return;\n      }\n      labelEl.textContent = state.preeditText;\n      labelEl.style.display = 'inline-block';\n      state.positionPreedit();\n    };\n\n    ta.addEventListener('compositionstart', function (ev) {\n      state.composing = true;\n      state.expectNextInput = false;\n      state.suppressNextInput = false;\n      state.updateModsFromEvent(ev);\n      state.setPreeditText('');\n      if (Module.solunaSetComposing) {\n        Module.solunaSetComposing(1);\n      }\n    });\n\n    ta.addEventListener('compositionupdate', function (ev) {\n      state.updateModsFromEvent(ev);\n      var text = (ev && typeof ev.data === 'string') ? ev.data : '';\n      state.setPreeditText(text);\n    });\n\n    ta.addEventListener('compositionend', function (ev) {\n      state.composing = false;\n      state.updateModsFromEvent(ev);\n      var text = (ev && ev.data) ? ev.data : '';\n      if (text.length > 0) {\n        state.commitText(text);\n        state.suppressNextInput = true;\n      } else {\n        state.expectNextInput = true;\n        if (Module._soluna_wasm_block_next_keypair) {\n          Module._soluna_wasm_block_next_keypair();\n        }\n      }\n      state.node.value = '';\n      state.hidePreedit();\n      if (Module.solunaSetComposing) {\n        Module.solunaSetComposing(0);\n      }\n    });\n\n    ta.addEventListener('input', function (ev) {\n      if (!state.active) {\n        return;\n      }\n      if (state.composing) {\n        return;\n      }\n      if (state.suppressNextInput) {\n        state.suppressNextInput = false;\n        state.node.value = '';\n        return;\n      }\n      if (!state.expectNextInput) {\n        return;\n      }\n      var text = (ev && typeof ev.data === 'string') ? ev.data : state.node.value;\n      if (text && text.length > 0) {\n        state.commitText(text);\n      }\n      state.expectNextInput = false;\n      state.node.value = '';\n    });\n\n    var updateMods = function (ev) {\n      state.updateModsFromEvent(ev);\n    };\n\n    var reposition = function () {\n      state.positionPreedit();\n    };\n\n    var ensureFocusOnTouch = function () {\n      if (!state.active) {\n        return;\n      }\n      var el = state.node;\n      if (!el) {\n        return;\n      }\n      el.style.display = 'block';\n      state.focusNode();\n    };\n\n    var clearFocusOnTouchEnd = function () {\n      if (state.active) {\n        return;\n      }\n      var el = state.node;\n      if (!el) {\n        return;\n      }\n      el.blur();\n      el.style.display = 'none';\n    };\n\n    globalScope.addEventListener('keydown', updateMods, true);\n    globalScope.addEventListener('keyup', updateMods, true);\n    globalScope.addEventListener('focus', function () {\n      state.queueBlur.cancel();\n      if (state.active) {\n        state.queueFocus();\n      }\n    }, true);\n    globalScope.addEventListener('blur', function () {\n      state.queueBlur.cancel();\n      state.queueFocus.cancel();\n      state.mods = 0;\n      state.composing = false;\n      state.expectNextInput = false;\n      state.suppressNextInput = false;\n      state.node.value = '';\n      state.hidePreedit();\n      if (Module.solunaSetComposing) {\n        Module.solunaSetComposing(0);\n      }\n    });\n    globalScope.addEventListener('resize', reposition);\n    globalScope.addEventListener('touchstart', ensureFocusOnTouch, true);\n    globalScope.addEventListener('touchend', clearFocusOnTouchEnd, true);\n    globalScope.addEventListener('touchcancel', clearFocusOnTouchEnd, true);\n    if (typeof document !== 'undefined') {\n      document.addEventListener('scroll', reposition, true);\n    }\n\n    Module.solunaIme = state;\n  },\n\n  soluna_wasm_dom_show__sig: 'vffff',\n  soluna_wasm_dom_show: function (x, y, w, h) {\n    var state = Module.solunaIme;\n    if (!state || typeof document === 'undefined') {\n      return;\n    }\n    var canvas = state.resolveCanvas();\n    if (!canvas) {\n      return;\n    }\n    var rect = canvas.getBoundingClientRect();\n    var scrollX = (typeof window !== 'undefined' && typeof window.scrollX === 'number') ? window.scrollX : 0;\n    var scrollY = (typeof window !== 'undefined' && typeof window.scrollY === 'number') ? window.scrollY : 0;\n    var canvasLeft = rect.left + scrollX;\n    var canvasTop = rect.top + scrollY;\n    var left = canvasLeft + x;\n    var top = canvasTop + y;\n    var width = Math.max(1, Number.isFinite(w) ? w : 1);\n    var height = Math.max(1, (Number.isFinite(h) && h > 0) ? h : 16);\n\n    state.rect.x = x;\n    state.rect.y = y;\n    state.rect.w = width;\n    state.rect.h = height;\n\n    var el = state.node;\n    el.style.display = 'block';\n    el.style.left = left + 'px';\n    el.style.top = top + 'px';\n    el.style.width = width + 'px';\n    el.style.height = height + 'px';\n    el.style.lineHeight = height + 'px';\n    if (!state.active) {\n      el.value = '';\n    }\n    state.active = true;\n    state.syncStylesFromCanvas(canvas, height);\n    state.positionPreedit();\n    state.queueFocus();\n  },\n\n  soluna_wasm_dom_hide__sig: 'v',\n  soluna_wasm_dom_hide: function () {\n    var state = Module.solunaIme;\n    if (!state || typeof document === 'undefined') {\n      return;\n    }\n    if (!state.active) {\n      return;\n    }\n    state.active = false;\n    state.queueFocus.cancel();\n    state.queueBlur.cancel();\n    state.composing = false;\n    state.expectNextInput = false;\n    state.suppressNextInput = false;\n    var el = state.node;\n    el.value = '';\n    el.style.display = 'none';\n    state.hidePreedit();\n    state.queueBlur();\n    if (Module.solunaSetComposing) {\n      Module.solunaSetComposing(0);\n    }\n  },\n\n  soluna_wasm_dom_set_font__deps: ['$UTF8ToString'],\n  soluna_wasm_dom_set_font__sig: 'vif',\n  soluna_wasm_dom_set_font: function (namePtr, size) {\n    var state = Module.solunaIme;\n    if (!state) {\n      return;\n    }\n    var resolvedName = null;\n    if (namePtr) {\n      try {\n        resolvedName = UTF8ToString(namePtr);\n      } catch (err) {\n        resolvedName = '';\n      }\n    }\n    if (resolvedName && resolvedName.length === 0) {\n      resolvedName = null;\n    }\n    var numericSize = 0;\n    if (Number.isFinite(size) && size > 0) {\n      numericSize = size;\n    }\n    var hasCustomFont = !!(resolvedName && resolvedName.length > 0);\n    state.customFont = hasCustomFont ? resolvedName : null;\n    state.customFontSize = numericSize;\n    if (!hasCustomFont && numericSize === 0) {\n      var canvas = state.resolveCanvas ? state.resolveCanvas() : null;\n      if (canvas) {\n        state.syncStylesFromCanvas(canvas, state.rect ? state.rect.h : 0);\n      } else {\n        state.applyFontOverride();\n      }\n    } else {\n      state.applyFontOverride();\n    }\n    state.positionPreedit();\n  },\n\n  soluna_wasm_dom_set_color__sig: 'vi',\n  soluna_wasm_dom_set_color: function (color) {\n    var state = Module.solunaIme;\n    if (!state) {\n      return;\n    }\n    var value = (color >>> 0);\n    if (value !== 0) {\n      var a = ((value >>> 24) & 0xff) / 255;\n      var r = (value >>> 16) & 0xff;\n      var g = (value >>> 8) & 0xff;\n      var b = value & 0xff;\n      state.customTextColor = 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';\n    } else {\n      state.customTextColor = null;\n    }\n    state.refreshColorFromContext();\n    state.positionPreedit();\n  }\n});\n"
  },
  {
    "path": "src/platform/wasm/soluna_openurl.js",
    "content": "mergeInto(LibraryManager.library, {\n  soluna_wasm_open_url__deps: ['$UTF8ToString'],\n  soluna_wasm_open_url__sig: 'vi',\n  soluna_wasm_open_url: function (urlPtr) {\n    if (!urlPtr) {\n      return;\n    }\n    var href = UTF8ToString(urlPtr);\n    if (!href) {\n      return;\n    }\n    if (typeof window !== 'undefined' && typeof window.open === 'function') {\n      window.open(href, '_blank');\n      return;\n    }\n    if (typeof self !== 'undefined' && typeof self.open === 'function') {\n      self.open(href, '_blank');\n    }\n  }\n});\n"
  },
  {
    "path": "src/platform/wasm/soluna_wasm_ime.h",
    "content": "#ifndef SOLUNA_WASM_IME_H\n#define SOLUNA_WASM_IME_H\n\n#if defined(__EMSCRIPTEN__)\n\n#include <emscripten/emscripten.h>\n#include <emscripten/html5.h>\n#if defined(__EMSCRIPTEN_PTHREADS__)\n#include <emscripten/threading.h>\n#endif\n#include <wchar.h>\n#include <uchar.h>\n#include <locale.h>\n#include <stdlib.h>\n#include <stdbool.h>\n#include <stdint.h>\n#include <string.h>\n\n#include \"ime_state.h\"\n#include \"ime_char_filter.h\"\n\nvoid soluna_emit_char(uint32_t codepoint, uint32_t modifiers, bool repeat);\nextern void soluna_wasm_setup_ime(void);\nextern void soluna_wasm_dom_show(float x, float y, float w, float h);\nextern void soluna_wasm_dom_hide(void);\nextern void soluna_wasm_dom_set_font(const char *name, float size);\nextern void soluna_wasm_dom_set_color(uint32_t color);\n\nstatic const int SOLUNA_WASM_CHAR_QUEUE_CAP = 32;\nstatic uint32_t g_soluna_wasm_expected_chars[32];\nstatic int g_soluna_wasm_expected_count = 0;\nstatic uint32_t g_soluna_wasm_ignore_chars[32];\nstatic int g_soluna_wasm_ignore_count = 0;\nstatic bool g_soluna_wasm_composing = false;\nstatic bool g_soluna_wasm_locale_ready = false;\nstatic int g_soluna_wasm_block_keys = 0;\n\nstatic inline struct soluna_ime_char_filter_state\nsoluna_wasm_char_filter_state(void) {\n    return (struct soluna_ime_char_filter_state) {\n        .expected_chars = g_soluna_wasm_expected_chars,\n        .expected_count = &g_soluna_wasm_expected_count,\n        .ignore_chars = g_soluna_wasm_ignore_chars,\n        .ignore_count = &g_soluna_wasm_ignore_count,\n        .capacity = SOLUNA_WASM_CHAR_QUEUE_CAP,\n    };\n}\n\nstatic void\nsoluna_wasm_call_setup(void) {\n#if defined(__EMSCRIPTEN_PTHREADS__)\n    if (!emscripten_is_main_browser_thread()) {\n        emscripten_async_run_in_main_runtime_thread(EM_FUNC_SIG_V, soluna_wasm_setup_ime);\n        return;\n    }\n#endif\n    soluna_wasm_setup_ime();\n}\n\nstatic void\nsoluna_wasm_call_show(float x, float y, float w, float h) {\n#if defined(__EMSCRIPTEN_PTHREADS__)\n    if (!emscripten_is_main_browser_thread()) {\n        emscripten_async_run_in_main_runtime_thread(EM_FUNC_SIG_VFFFF, soluna_wasm_dom_show, x, y, w, h);\n        return;\n    }\n#endif\n    soluna_wasm_dom_show(x, y, w, h);\n}\n\nstatic void\nsoluna_wasm_call_hide(void) {\n#if defined(__EMSCRIPTEN_PTHREADS__)\n    if (!emscripten_is_main_browser_thread()) {\n        emscripten_async_run_in_main_runtime_thread(EM_FUNC_SIG_V, soluna_wasm_dom_hide);\n        return;\n    }\n#endif\n    soluna_wasm_dom_hide();\n}\n\nstatic void\nsoluna_wasm_call_set_font(const char *name, float size) {\n#if defined(__EMSCRIPTEN_PTHREADS__)\n    if (!emscripten_is_main_browser_thread()) {\n        emscripten_async_run_in_main_runtime_thread(EM_FUNC_SIG_VIF, soluna_wasm_dom_set_font, (intptr_t)name, size);\n        return;\n    }\n#endif\n    soluna_wasm_dom_set_font(name, size);\n}\n\nstatic void\nsoluna_wasm_call_set_color(uint32_t color) {\n#if defined(__EMSCRIPTEN_PTHREADS__)\n    if (!emscripten_is_main_browser_thread()) {\n        emscripten_async_run_in_main_runtime_thread(EM_FUNC_SIG_VI, soluna_wasm_dom_set_color, color);\n        return;\n    }\n#endif\n    soluna_wasm_dom_set_color(color);\n}\n\nstatic void\nsoluna_wasm_reset_queues(void) {\n    soluna_ime_char_filter_reset(soluna_wasm_char_filter_state());\n}\n\nvoid\nsoluna_wasm_set_font(const char *name, float size) {\n    soluna_wasm_call_setup();\n    soluna_wasm_call_set_font(name, size);\n}\n\nstatic void\nsoluna_wasm_ensure_locale(void) {\n    if (g_soluna_wasm_locale_ready) {\n        return;\n    }\n    if (!setlocale(LC_CTYPE, \"C.UTF-8\")) {\n        if (!setlocale(LC_CTYPE, \"en_US.UTF-8\")) {\n            setlocale(LC_CTYPE, \"C\");\n        }\n    }\n    g_soluna_wasm_locale_ready = true;\n}\n\nstatic void\nsoluna_wasm_emit_utf8(const char *text, uint32_t mods) {\n    if (!text || text[0] == '\\0') {\n        return;\n    }\n    soluna_wasm_ensure_locale();\n    mbstate_t state;\n    memset(&state, 0, sizeof(state));\n    const char *ptr = text;\n    while (*ptr) {\n        char32_t ch = 0;\n        size_t consumed = mbrtoc32(&ch, ptr, MB_CUR_MAX, &state);\n        if (consumed == (size_t)-1 || consumed == (size_t)-2) {\n            memset(&state, 0, sizeof(state));\n            ++ptr;\n            continue;\n        }\n        if (consumed == 0) {\n            consumed = 1;\n        }\n        soluna_ime_char_filter_push_expected(soluna_wasm_char_filter_state(), (uint32_t)ch);\n        soluna_emit_char((uint32_t)ch, mods, false);\n        ptr += consumed;\n    }\n}\n\nstatic inline bool\nsoluna_wasm_is_composing(void) {\n    return g_soluna_wasm_composing;\n}\n\nvoid\nsoluna_wasm_hide(void) {\n    soluna_wasm_reset_queues();\n    soluna_wasm_call_hide();\n    g_soluna_wasm_composing = false;\n}\n\nvoid\nsoluna_wasm_apply_rect(void) {\n    if (!g_soluna_ime_rect.valid) {\n        soluna_wasm_hide();\n        return;\n    }\n    soluna_wasm_call_setup();\n    soluna_wasm_call_set_color(g_soluna_ime_rect.text_color);\n    soluna_wasm_call_show(g_soluna_ime_rect.x, g_soluna_ime_rect.y, g_soluna_ime_rect.w, g_soluna_ime_rect.h);\n}\n\nstatic inline bool\nsoluna_wasm_should_block_key_event(const sapp_event *ev) {\n    if (!ev) {\n        return false;\n    }\n    bool is_key_event = ev->type == SAPP_EVENTTYPE_KEY_DOWN || ev->type == SAPP_EVENTTYPE_KEY_UP;\n    if (g_soluna_wasm_block_keys > 0 && is_key_event) {\n        --g_soluna_wasm_block_keys;\n        return true;\n    }\n    if (!soluna_wasm_is_composing()) {\n        return false;\n    }\n    return is_key_event;\n}\n\nstatic inline bool\nsoluna_wasm_filter_char_event(const sapp_event *ev) {\n    if (!ev || ev->type != SAPP_EVENTTYPE_CHAR) {\n        return false;\n    }\n    return soluna_ime_char_filter_should_skip(soluna_wasm_char_filter_state(), ev->char_code);\n}\n\nstatic inline void\nsoluna_wasm_handle_event(const sapp_event *ev) {\n    if (!ev) {\n        return;\n    }\n    switch (ev->type) {\n    case SAPP_EVENTTYPE_UNFOCUSED:\n        soluna_wasm_hide();\n        break;\n    case SAPP_EVENTTYPE_FOCUSED:\n    case SAPP_EVENTTYPE_RESIZED:\n        if (g_soluna_ime_rect.valid) {\n            soluna_wasm_apply_rect();\n        }\n        break;\n    default:\n        break;\n    }\n}\n\nEMSCRIPTEN_KEEPALIVE void\nsoluna_wasm_ime_commit(const char *text, int modifiers) {\n    soluna_wasm_emit_utf8(text, (uint32_t)modifiers);\n}\n\nEMSCRIPTEN_KEEPALIVE void\nsoluna_wasm_set_composing(int active) {\n    g_soluna_wasm_composing = (active != 0);\n}\n\nEMSCRIPTEN_KEEPALIVE void\nsoluna_wasm_block_next_keypair(void) {\n    g_soluna_wasm_block_keys = 2;\n}\n\n#endif /* __EMSCRIPTEN__ */\n\n#endif /* SOLUNA_WASM_IME_H */\n"
  },
  {
    "path": "src/platform/windows/soluna_windows_ime.c",
    "content": "#define WIN32_LEAN_AND_MEAN\n#include <windows.h>\n#include <imm.h>\n#include <windowsx.h>\n#include <winnls.h>\n\n\n#include <stdio.h>\n\n#include \"sokol/sokol_app.h\"\n\n#include \"ime_state.h\"\n#include \"soluna_windows_ime.h\"\n\nstatic WNDPROC g_soluna_prev_wndproc = NULL;\nstatic BOOL g_soluna_wndproc_installed = FALSE;\nstatic LOGFONTW g_soluna_ime_font;\nstatic BOOL g_soluna_ime_font_valid = FALSE;\nstatic BOOL g_soluna_composition = FALSE;\n\nstatic void\nsoluna_win32_set_candidate_position(HIMC imc, LONG caret_x, LONG caret_y, LONG caret_w, LONG caret_h) {\n    RECT exclude_rect;\n    exclude_rect.left = caret_x;\n    exclude_rect.top = caret_y;\n    exclude_rect.right = caret_x + caret_w;\n    exclude_rect.bottom = caret_y + caret_h;\n\n    MapWindowPoints((HWND)sapp_win32_get_hwnd(), NULL, (LPPOINT)&exclude_rect, 2);\n\n    CANDIDATEFORM cand;\n    memset(&cand, 0, sizeof(cand));\n    cand.dwIndex = 0;\n    cand.dwStyle = CFS_EXCLUDE;\n    cand.rcArea = exclude_rect;\n    cand.ptCurrentPos.x = exclude_rect.left;\n    cand.ptCurrentPos.y = exclude_rect.bottom;\n    ImmSetCandidateWindow(imc, &cand);\n}\n\nvoid\nsoluna_win32_apply_ime_rect(void) {\n    HWND hwnd = (HWND)sapp_win32_get_hwnd();\n    if (!hwnd) {\n        return;\n    }\n    HIMC imc = ImmGetContext(hwnd);\n    if (!imc) {\n        fprintf(stderr, \"ImmGetContext failed\\n\");\n        return;\n    }\n    if (g_soluna_ime_rect.valid) {\n        float scale = sapp_dpi_scale();\n        if (scale <= 0.0f) {\n            scale = 1.0f;\n        }\n        float rect_top = g_soluna_ime_rect.y;\n        if (rect_top < 0.0f) {\n            rect_top = 0.0f;\n        }\n        float rect_height = (g_soluna_ime_rect.h > 0.0f ? g_soluna_ime_rect.h : 1.0f);\n        float win_height = (float)sapp_height();\n        float rect_bottom = rect_top + rect_height;\n        if (win_height > 0.0f && rect_bottom > win_height) {\n            rect_bottom = win_height;\n        }\n        float actual_height = rect_bottom - rect_top;\n        if (actual_height <= 0.0f) {\n            actual_height = 1.0f;\n        }\n\n        LONG caret_x = (LONG)(g_soluna_ime_rect.x * scale + 0.5f);\n        LONG caret_y = (LONG)(rect_top * scale + 0.5f);\n        LONG caret_w = (LONG)((g_soluna_ime_rect.w > 0.0f ? g_soluna_ime_rect.w : 1.0f) * scale + 0.5f);\n        LONG caret_h = (LONG)(actual_height * scale + 0.5f);\n\n        COMPOSITIONFORM cf;\n        memset(&cf, 0, sizeof(cf));\n        cf.dwStyle = CFS_POINT;\n        cf.ptCurrentPos.x = caret_x;\n        cf.ptCurrentPos.y = caret_y;\n        ImmSetCompositionWindow(imc, &cf);\n\n        soluna_win32_set_candidate_position(imc, caret_x, caret_y, caret_w, caret_h);\n\n        if (g_soluna_ime_font_valid) {\n            LOGFONTW lf = g_soluna_ime_font;\n            if (lf.lfHeight == 0) {\n                lf.lfHeight = -(LONG)caret_h;\n            }\n            ImmSetCompositionFontW(imc, &lf);\n        }\n    }\n    ImmReleaseContext(hwnd, imc);\n}\n\nstatic LRESULT CALLBACK\nsoluna_win32_wndproc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {\n    switch (msg) {\n    case WM_IME_COMPOSITION:\n    case WM_IME_STARTCOMPOSITION:\n        g_soluna_composition = TRUE;\n        if (g_soluna_ime_rect.valid) {\n            soluna_win32_apply_ime_rect();\n        }\n        break;\n    case WM_IME_ENDCOMPOSITION:\n        g_soluna_composition = FALSE;\n        break;\n    case WM_DESTROY:\n        g_soluna_composition = FALSE;\n        if (g_soluna_prev_wndproc) {\n            SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)g_soluna_prev_wndproc);\n            g_soluna_prev_wndproc = NULL;\n            g_soluna_wndproc_installed = FALSE;\n        }\n        break;\n    case WM_KEYDOWN:\n    case WM_KEYUP:\n        if (g_soluna_composition) {\n            return TRUE;\n        }\n        break;\n    default:\n        break;\n    }\n    if (g_soluna_prev_wndproc) {\n        return CallWindowProc(g_soluna_prev_wndproc, hwnd, msg, wParam, lParam);\n    }\n    return DefWindowProc(hwnd, msg, wParam, lParam);\n}\n\nvoid\nsoluna_win32_install_wndproc(void) {\n    if (g_soluna_wndproc_installed) {\n        return;\n    }\n    HWND hwnd = (HWND)sapp_win32_get_hwnd();\n    if (!hwnd) {\n        return;\n    }\n    WNDPROC prev = (WNDPROC)SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)soluna_win32_wndproc);\n    if (prev) {\n        g_soluna_prev_wndproc = prev;\n        g_soluna_wndproc_installed = TRUE;\n    }\n}\n\nvoid\nsoluna_win32_set_ime_font(const char *font_name, float height_px) {\n    float scale = sapp_dpi_scale();\n    if (scale <= 0.0f) {\n        scale = 1.0f;\n    }\n    LOGFONTW lf;\n    memset(&lf, 0, sizeof(lf));\n    lf.lfCharSet = DEFAULT_CHARSET;\n    lf.lfQuality = CLEARTYPE_QUALITY;\n    if (height_px > 0.0f) {\n        lf.lfHeight = -(LONG)(height_px * scale + 0.5f);\n    }\n    if (font_name && font_name[0]) {\n        int wlen = MultiByteToWideChar(CP_UTF8, 0, font_name, -1, NULL, 0);\n        if (wlen > 0 && wlen <= (int)(sizeof(lf.lfFaceName) / sizeof(wchar_t))) {\n            MultiByteToWideChar(CP_UTF8, 0, font_name, -1, lf.lfFaceName, wlen);\n        }\n    }\n    g_soluna_ime_font = lf;\n    g_soluna_ime_font_valid = TRUE;\n}\n\nvoid\nsoluna_win32_reset_ime_font(void) {\n    g_soluna_ime_font_valid = FALSE;\n}\n"
  },
  {
    "path": "src/platform/windows/soluna_windows_ime.h",
    "content": "#ifndef SOLUNA_WINDOWS_IME_H\n#define SOLUNA_WINDOWS_IME_H\n\nvoid soluna_win32_install_wndproc(void);\nvoid soluna_win32_apply_ime_rect(void);\nvoid soluna_win32_set_ime_font(const char *font_name, float height_px);\nvoid soluna_win32_reset_ime_font(void);\n\n#endif /* SOLUNA_WINDOWS_IME_H */\n"
  },
  {
    "path": "src/render.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n#include <string.h>\n#include <stdint.h>\n\n#include \"sokol/sokol_gfx.h\"\n#include \"sokol/sokol_glue.h\"\n#include \"sokol/sokol_app.h\"\n#include \"texquad.glsl.h\"\n#include \"srbuffer.h\"\n#include \"sprite_submit.h\"\n#include \"batch.h\"\n#include \"spritemgr.h\"\n\n#define UNIFORM_MAX 4\n#define BINDINGNAME_MAX 32\n\nstruct buffer {\n\tsg_buffer handle;\n\tstruct sg_buffer_usage usage;\n};\n\nstruct image {\n\tsg_image img;\n\tint size;\n};\n\nstruct sampler {\n\tsg_sampler handle;\n};\n\nstatic struct sg_buffer_usage\nget_buffer_type(lua_State *L, int index) {\n\tif (lua_getfield(L, index, \"type\") != LUA_TSTRING) {\n\t\tluaL_error(L, \"Need .type\");\n\t}\n\tconst char * str = lua_tostring(L, -1);\n\tstruct sg_buffer_usage usage = { 0 };\n\tif (strcmp(str, \"vertex\") == 0) {\n\t\tusage.vertex_buffer = true;\n\t} else if (strcmp(str, \"index\") == 0) {\n\t\tusage.index_buffer = true;\n\t} else if (strcmp(str, \"storage\") == 0) {\n\t    usage.storage_buffer = true;\n\t} else {\n\t\tluaL_error(L, \"Invalid buffer .type = %s\", str);\n\t}\n\tlua_pop(L, 1);\n\treturn usage;\n}\n\nstatic void\nget_buffer_usage(lua_State *L, int index, struct sg_buffer_usage *usage) {\n\tif (lua_getfield(L, index, \"usage\") != LUA_TSTRING) {\n\t\tif (lua_isnil(L, -1)) {\n\t\t\tlua_pop(L, 1);\n\t\t\tusage->immutable = true;\n\t\t\treturn;\n\t\t}\n\t\tluaL_error(L, \"Invalid .usage\");\n\t}\n\tconst char * str = lua_tostring(L, -1);\n\tif (strcmp(str, \"stream\") == 0) {\n\t\tusage->stream_update = true;\n\t} else if (strcmp(str, \"dynamic\") == 0) {\n\t\tusage->dynamic_update = true;\n\t} else if (strcmp(str, \"immutable\") == 0) {\n\t\tusage->immutable = true;\n\t} else {\n\t\tluaL_error(L, \"Invalid buffer .usage = %s\", str);\n\t}\n\tlua_pop(L, 1);\n}\n\nstatic const void *\nget_buffer_data(lua_State *L, int index, size_t *sz) {\n\tint t = lua_getfield(L, index, \"data\");\n\tif (t == LUA_TNIL) {\n\t\t// no ptr\n\t\tlua_pop(L, 1);\n\t\tif (lua_getfield(L, index, \"size\") != LUA_TNUMBER) {\n\t\t\tluaL_error(L, \"No .data and .size\");\n\t\t}\n\t\t*sz = luaL_checkinteger(L, -1);\n\t\tlua_pop(L, 1);\n\t\treturn NULL;\n\t}\n\tsize_t size = 0;\n\tif (lua_getfield(L, index, \"size\") == LUA_TNUMBER) {\n\t\tsize = luaL_checkinteger(L, -1);\n\t}\n\tlua_pop(L, 1);\n\tif (t == LUA_TLIGHTUSERDATA) {\n\t\tif (size == 0) {\n\t\t\tluaL_error(L, \"lightuserdata for .data without .size\");\n\t\t}\n\t\t*sz = size;\n\t\tconst void * ptr = lua_touserdata(L, -1);\n\t\tlua_pop(L, 1);\n\t\treturn ptr;\n\t}\n\telse if (t == LUA_TUSERDATA) {\n\t\tsize_t rawlen = lua_rawlen(L, -1);\n\t\tif (size > 0 && size != rawlen)\n\t\t\tluaL_error(L, \"size of userdata %d != %d\", rawlen, size);\n\t\tconst void * ptr = lua_touserdata(L, -1);\n\t\tlua_pop(L, 1);\n\t\t*sz = size;\n\t\treturn ptr;\n\t} else if (t == LUA_TSTRING) {\n\t\tsize_t rawlen;\n\t\tconst void * ptr = (const void *)lua_tolstring(L, -1, &rawlen);\n\t\tif (size > 0 && size != rawlen)\n\t\t\tluaL_error(L, \"size of string %d != %d\", rawlen, size);\n\t\tlua_pop(L, 1);\n\t\t*sz = rawlen;\n\t\treturn ptr;\n\t}\n\tluaL_error(L, \"Invalid .data type = %s\", lua_typename(L, t));\n\t*sz = 0;\n\treturn NULL;\n}\n\nstatic int\nlbuffer_update(lua_State *L) {\n\tif (lua_gettop(L) == 1)\n\t\treturn 0;\n\tstruct buffer *p = (struct buffer *)luaL_checkudata(L, 1, \"SOKOL_BUFFER\");\n\tsize_t sz;\n\tconst void *ptr;\n\tswitch (lua_type(L, 2)) {\n\tcase LUA_TSTRING:\n\t\tptr = (const void *)lua_tolstring(L, 2, &sz);\n\t\tbreak;\n\tcase LUA_TUSERDATA:\n\t\tptr = (const void *)lua_touserdata(L, 2);\n\t\tsz = lua_rawlen(L, 2);\n\t\tif (lua_isinteger(L, 3)) {\n\t\t\tint usersize = lua_tointeger(L, 3);\n\t\t\tif (usersize > sz)\n\t\t\t\treturn luaL_error(L, \"Invalid size %d > %d\", usersize, sz);\n\t\t\tsz = usersize;\n\t\t}\n\t\tbreak;\n\tcase LUA_TLIGHTUSERDATA:\n\t\tptr = (const void *)lua_touserdata(L, 2);\n\t\tsz = luaL_checkinteger(L, 3);\n\t\tbreak;\n\tdefault:\n\t\treturn luaL_error(L, \"Invalid data type %s\", lua_typename(L, lua_type(L, 2)));\n\t}\n\tsg_update_buffer(p->handle, &(sg_range) { ptr, sz });\n\treturn 0;\n}\n\nstatic int\nlbuffer_ref(lua_State *L) {\n\tstruct buffer *p = (struct buffer *)lua_touserdata(L, 1);\n\tluaL_checktype(L, 2, LUA_TLIGHTUSERDATA);\n\tsg_buffer *ref = (sg_buffer *)lua_touserdata(L, 2);\n\t*ref = p->handle;\n\treturn 0;\n}\n\nstatic int\nlbuffer_tostring(lua_State *L) {\n\tstruct buffer *p = (struct buffer *)lua_touserdata(L, 1);\n\tconst char * name = \"Invalid\";\n\tif (p->usage.vertex_buffer) {\n\t\tname = \"VB\";\n\t} else if (p->usage.index_buffer) {\n\t\tname = \"IB\";\n\t} else if (p->usage.storage_buffer) {\n\t\tname = \"SB\";\n\t}\n\tlua_pushfstring(L, \"[%s:%d]\", name, p->handle.id);\n\treturn 1;\n}\n\nstatic int\nlbuffer(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TTABLE);\n\tstruct buffer * p = (struct buffer *)lua_newuserdatauv(L, sizeof(*p), 0);\n\tp->usage = get_buffer_type(L, 1);\n\tget_buffer_usage(L, 1, &p->usage);\n\tsize_t sz;\n\tconst void *ptr = get_buffer_data(L, 1, &sz);\n\tif (p->usage.immutable && ptr == NULL) {\n\t\treturn luaL_error(L, \"immutable buffer needs init data\");\n\t}\n\tconst char *label = NULL;\n\tif (lua_getfield(L, 1, \"label\") == LUA_TSTRING) {\n\t\tlabel = lua_tostring(L, -1);\n\t}\n\tlua_pop(L, 1);\n\tp->handle = sg_make_buffer(&(sg_buffer_desc) {\n\t\t.size = sz,\n\t\t.usage = p->usage,\n\t\t.label = label,\n\t    .data.ptr = ptr,\n\t\t.data.size = ptr == NULL ? 0 : sz,\n\t});\n\t\t\n\tif (luaL_newmetatable(L, \"SOKOL_BUFFER\")) {\n\t\tluaL_Reg l[] = {\n\t\t\t{ \"__index\", NULL },\n\t\t\t{ \"__call\", lbuffer_ref },\n\t\t\t{ \"__tostring\", lbuffer_tostring },\n\t\t\t{ \"update\", lbuffer_update },\n\t\t\t{ NULL, NULL },\n\t\t};\n\t\tluaL_setfuncs(L, l, 0);\n\n\t\tlua_pushvalue(L, -1);\n\t\tlua_setfield(L, -2, \"__index\");\n\t}\n\tlua_setmetatable(L, -2);\n\n\treturn 1;\n}\n\n// todo : offscreen pass\nstruct pass {\n\tsg_pass pass;\n\tint swapchain;\n};\n\nstatic int\nread_color_action(lua_State *L, int index, sg_pass_action *action, int idx) {\n\tchar key[] = { 'c', 'o', 'l', 'o' , 'r' , '0' + idx, '\\0' };\n\tint t = lua_getfield(L, index, key);\n\tif (t == LUA_TNIL) {\n\t\tlua_pop(L, 1);\n\t\treturn 0;\n\t}\n\tif (idx >= SG_MAX_COLOR_ATTACHMENTS)\n\t\treturn luaL_error(L, \"Too many color attachments %d >= %d\", idx , SG_MAX_COLOR_ATTACHMENTS);\n\tif ( t == LUA_TSTRING ) {\n\t\tconst char * key = lua_tostring(L, -1);\n\t\tif (strcmp(key, \"load\") == 0) {\n\t\t\taction->colors[idx].load_action = SG_LOADACTION_LOAD;\n\t\t} else if (strcmp(key, \"dontcare\") == 0 ) {\n\t\t\taction->colors[idx].load_action = SG_LOADACTION_DONTCARE;\n\t\t} else {\n\t\t\treturn luaL_error(L, \"Invalid load action (%d) = %s\", idx, key);\n\t\t}\n\t} else {\n\t\tuint32_t c = luaL_checkinteger(L, -1);\n\t\tif (c <= 0xffffff) {\n\t\t\taction->colors[idx].clear_value.a = 1.0f;\n\t\t} else {\n\t\t\taction->colors[idx].clear_value.a = ((c & 0xff000000) >> 24) / 255.0f;\n\t\t}\n\t\taction->colors[idx].clear_value.r = ((c & 0xff0000) >> 16) / 255.0f;\n\t\taction->colors[idx].clear_value.g = ((c & 0x00ff00) >> 8) / 255.0f;\n\t\taction->colors[idx].clear_value.b = ((c & 0x0000ff)) / 255.0f;\n\t\taction->colors[idx].load_action = SG_LOADACTION_CLEAR;\n\t}\n\tlua_pop(L, 1);\n\treturn 1;\n}\n\nstatic int\nlpass_begin(lua_State *L) {\n\tstruct pass * p = (struct pass *)luaL_checkudata(L, 1, \"SOKOL_PASS\");\n\tif (p->swapchain) {\n\t\tp->pass.swapchain = sglue_swapchain();\n\t}\n\tsg_begin_pass(&p->pass);\n\treturn 0;\n}\n\nstatic int\nlpass_end(lua_State *L) {\n\tsg_end_pass();\n\treturn 0;\n}\n\nstatic void\nread_attachments(lua_State *L, sg_attachments *attachments) {\n\tluaL_checktype(L, -1, LUA_TTABLE);\n\tint i;\n\tfor (i = 0; i < SG_MAX_COLOR_ATTACHMENTS; i++) {\n\t\tchar key[] = { 'c', 'o', 'l', 'o' , 'r' , '0' + i, '\\0' };\n\t\tif (lua_getfield(L, -1, key) == LUA_TNIL) {\n\t\t\tlua_pop(L, 1);\n\t\t\tbreak;\n\t\t}\n\t\tluaL_checkudata(L, -1, \"SOKOL_VIEW\");\n\t\tlua_call(L, 0, 1);\n\t\tsg_view *color_v = (sg_view *)lua_touserdata(L, -1);\n\t\tif (color_v == NULL) {\n\t\t\tluaL_error(L, \"Invalid %s view\", key);\n\t\t}\n\t\tattachments->colors[i] = *color_v;\n\t\tlua_pop(L, 1);\n\t}\n\n\tlua_getfield(L, -1, \"depth_stencil\");\n\tluaL_checkudata(L, -1, \"SOKOL_VIEW\");\n\tlua_call(L, 0, 1);\n\tsg_view *ds_v = (sg_view *)lua_touserdata(L, -1);\n\tif (ds_v == NULL) {\n\t\tluaL_error(L, \"Invalid depth_stencil view\");\n\t}\n\tattachments->depth_stencil = *ds_v;\n\tlua_pop(L, 1);\n}\n\nstatic int\nlpass_new(lua_State *L) {\n\tstruct pass * p = lua_newuserdatauv(L, sizeof(*p), 0);\n\tmemset(p, 0, sizeof(*p));\n\tluaL_checktype(L, 1, LUA_TTABLE);\n\tif (lua_getfield(L, 1, \"swapchain\") == LUA_TBOOLEAN && lua_toboolean(L, -1)) {\n\t\tp->swapchain = true;\n\t}\n\tlua_pop(L, 1);\n\n\tsg_pass_action *action = &p->pass.action;\n\t\n\tint i = 0;\n\twhile (read_color_action(L, 1, action, i)) {\n\t\t++i;\n\t}\n\tif (lua_getfield(L, 1, \"depth\") == LUA_TNIL) {\n\t\taction->depth.load_action = SG_LOADACTION_DONTCARE;\n\t} else {\n\t\tfloat depth = luaL_checknumber(L, -1);\n\t\taction->depth.load_action = SG_LOADACTION_CLEAR;\n\t\taction->depth.clear_value = depth;\n\t}\n\tlua_pop(L, 1);\n\tif (lua_getfield(L, 1, \"stencil\") == LUA_TNIL) {\n\t\taction->depth.load_action = SG_LOADACTION_DONTCARE;\n\t} else {\n\t\tint s = luaL_checkinteger(L, -1);\n\t\tif (s < 0 || s > 255)\n\t\t\treturn luaL_error(L, \"Invalid stencil %d\", s);\n\t\taction->depth.load_action = SG_LOADACTION_CLEAR;\n\t\taction->depth.clear_value = s;\n\t}\n\tlua_pop(L, 1);\n\tif (lua_getfield(L, 1, \"attachment\") != LUA_TNIL) {\n\t\tif (p->swapchain) {\n\t\t\treturn luaL_error(L, \"swapchain not allows with attachment\");\n\t\t}\n\t\tread_attachments(L, &p->pass.attachments);\n\t} else if (!p->swapchain) {\n\t\treturn luaL_error(L, \"missing swapchain\");\n\t}\n\tlua_pop(L, 1);\n\tif (luaL_newmetatable(L, \"SOKOL_PASS\")) {\n\t\tluaL_Reg l[] = {\n\t\t\t{ \"__index\", NULL },\n\t\t\t{ \"begin\", lpass_begin },\n\t\t\t{ \"finish\", lpass_end },\t// end is a reserved keyword in lua, use finish instead\n\t\t\t{ NULL, NULL },\n\t\t};\n\t\tluaL_setfuncs(L, l, 0);\n\t\t\n\t\tlua_pushvalue(L, -1);\n\t\tlua_setfield(L, -2, \"__index\");\n\t}\n\tlua_setmetatable(L, -2);\n\treturn 1;\n}\n\nstatic int\nlsubmit(lua_State *L) {\n\tsg_commit();\n\treturn 0;\n}\n\nstatic int\nlimage_update(lua_State *L) {\n\tstruct image *p = (struct image *)luaL_checkudata(L, 1, \"SOKOL_IMAGE\");\n\t// todo: support subimage\n\tvoid *buffer = lua_touserdata(L, 2);\n\tif (buffer == NULL)\n\t\treturn luaL_error(L, \"Need data\");\n\tsg_image_data data = {\n\t\t.mip_levels[0].ptr = buffer,\n\t\t.mip_levels[0].size = p->size,\n\t};\n\tsg_update_image(p->img, &data);\n\treturn 0;\n}\n\nstatic int\nget_pixel_format(lua_State *L, const char * type, int *pixel_size) {\n\tif (strcmp(type, \"RGBA8\") == 0) {\n\t\t*pixel_size = 4;\n\t\treturn SG_PIXELFORMAT_RGBA8;\n\t} else if (strcmp(type, \"R8\") == 0) {\n\t\t*pixel_size = 1;\n\t\treturn SG_PIXELFORMAT_R8;\n\t} else if (strcmp(type, \"DEPTH\") == 0) {\n\t\t*pixel_size = 0;\n\t\treturn SG_PIXELFORMAT_DEPTH;\n\t}\n\treturn luaL_error(L, \"Invalid pixel format %s\", type);\n}\n\nstatic int\nlimage_ref(lua_State *L) {\n\tstruct image *p = (struct image *)lua_touserdata(L, 1);\n\tluaL_checktype(L, 2, LUA_TLIGHTUSERDATA);\n\tsg_image *ref = (sg_image *)lua_touserdata(L, 2);\n\t*ref = p->img;\n\treturn 0;\n}\n\nstatic int\nlimage(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TTABLE);\n\tsg_image_desc img = { .usage.dynamic_update = true };\n\tif (lua_getfield(L, 1, \"width\") != LUA_TNUMBER) {\n\t\treturn luaL_error(L, \"Need .width\");\n\t}\n\timg.width = luaL_checkinteger(L, -1);\n\tlua_pop(L, 1);\n\tif (lua_getfield(L, 1, \"height\") != LUA_TNUMBER) {\n\t\treturn luaL_error(L, \"Need .height\");\n\t}\n\timg.height = luaL_checkinteger(L, -1);\n\tlua_pop(L, 1);\n\tif (lua_getfield(L, 1, \"label\") == LUA_TSTRING) {\n\t\timg.label = lua_tostring(L, -1);\n\t}\n\tlua_pop(L, 1);\n\tint pixel_size = 4;\n\tif (lua_getfield(L, 1, \"pixel_format\") == LUA_TSTRING) {\n\t\timg.pixel_format = get_pixel_format(L, lua_tostring(L, -1), &pixel_size);\n\t} else {\n\t\timg.pixel_format = SG_PIXELFORMAT_RGBA8;\n\t\tpixel_size = 4;\n\t}\n\tlua_pop(L, 1);\n\tif (lua_getfield(L, 1, \"color_attachment\") == LUA_TBOOLEAN && lua_toboolean(L, -1)) {\n\t\timg.pixel_format = 0;\n\t\timg.usage.color_attachment = 1;\n\t\timg.usage.dynamic_update = 0;\n\t}\n\tlua_pop(L, 1);\n\tif (lua_getfield(L, 1, \"depth_stencil_attachment\") == LUA_TBOOLEAN && lua_toboolean(L, -1)) {\n\t\timg.pixel_format = 0;\n\t\timg.usage.depth_stencil_attachment = 1;\n\t\timg.usage.dynamic_update = 0;\n\t}\n\tlua_pop(L, 1);\n\t// todo: type, render_target, num_slices, num_mipmaps, pixel_format, etc\n\tstruct image * p = (struct image *)lua_newuserdatauv(L, sizeof(*p), 0);\n\tmemset(p, 0, sizeof(*p));\n\tif (luaL_newmetatable(L, \"SOKOL_IMAGE\")) {\n\t\tluaL_Reg l[] = {\n\t\t\t{ \"__index\", NULL },\n\t\t\t{ \"__call\", limage_ref },\n\t\t\t{ \"update\", limage_update },\n\t\t\t{ NULL, NULL },\n\t\t};\n\t\tluaL_setfuncs(L, l, 0);\n\n\t\tlua_pushvalue(L, -1);\n\t\tlua_setfield(L, -2, \"__index\");\n\t}\n\tlua_setmetatable(L, -2);\n\tp->img = sg_make_image(&img);\n\tp->size = img.width * img.height * pixel_size;\n\treturn 1;\n}\n\nstatic int\nlsampler_ref(lua_State *L) {\n\tstruct sampler *p = (struct sampler *)lua_touserdata(L, 1);\n\tluaL_checktype(L, 2, LUA_TLIGHTUSERDATA);\n\tsg_sampler *ref = (sg_sampler *)lua_touserdata(L, 2);\n\t*ref = p->handle;\n\treturn 0;\n}\n\nstruct enum_string {\n\tconst char *name;\n\tint v;\n};\n\nstatic int\nconvert_enum(lua_State *L, struct enum_string *es, const char *name) {\n\twhile (es->name) {\n\t\tif (strcmp(name, es->name) == 0) {\n\t\t\treturn es->v;\n\t\t}\n\t\t++es;\n\t}\n\treturn luaL_error(L, \"Invalid enum %s\", name);\n}\n\nstatic int\nread_enum(lua_State *L, const char *key, struct enum_string *es) {\n\tint r = 0;\n\tswitch (lua_getfield(L, 1, key)) {\n\t\tcase LUA_TNIL:\n\t\t\tbreak;\n\t\tcase LUA_TSTRING:\n\t\t\tr = convert_enum(L, es, lua_tostring(L, -1));\n\t\t\tbreak;\n\t\tdefault :\n\t\t\treturn luaL_error(L, \"Invalid .%s (should be a string, it's %s)\", key,\n\t\t\t\tlua_typename(L, lua_type(L, -1)));\n\t}\n\tlua_pop(L, 1);\n\treturn r;\n}\n\nstatic int\nlsampler(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TTABLE);\n\tstruct sampler * s = (struct sampler *)lua_newuserdatauv(L, sizeof(*s), 0);\n\tstruct sg_sampler_desc desc = { 0 };\n\tif (lua_getfield(L, 1, \"label\") == LUA_TSTRING) {\n\t\tdesc.label = lua_tostring(L, -1);\n\t}\n\tlua_pop(L, 1);\n\tstatic struct enum_string filter[] = {\n\t\t{ \"nearest\", SG_FILTER_NEAREST },\n\t\t{ \"linear\", SG_FILTER_LINEAR },\n\t\t{ NULL, 0 },\n\t};\n\tdesc.min_filter = read_enum(L, \"min_filter\", filter);\n\tdesc.mag_filter = read_enum(L, \"mag_filter\", filter);\n\tdesc.mipmap_filter = read_enum(L, \"mipmap_filter\", filter);\n\t\n\tstatic struct enum_string wrap[] = {\n\t\t{ \"repeat\", SG_WRAP_REPEAT },\n\t\t{ \"edge\", SG_WRAP_CLAMP_TO_EDGE },\n\t\t{ \"border\", SG_WRAP_CLAMP_TO_BORDER },\n\t\t{ \"mirror\", SG_WRAP_MIRRORED_REPEAT },\n\t\t{ NULL, 0 },\n\t};\n\tdesc.wrap_u = read_enum(L, \"wrap_u\", wrap);\n\tdesc.wrap_v = read_enum(L, \"wrap_v\", wrap);\n\tdesc.wrap_w = read_enum(L, \"wrap_w\", wrap);\n\t\n\t// todo : set min_lod/max_lod/border_color etc.\n\t\n\ts->handle = sg_make_sampler(&desc);\n\t\n\tif (luaL_newmetatable(L, \"SOKOL_SAMPLER\")) {\n\t\tlua_pushcfunction(L, lsampler_ref ),\n\t\tlua_setfield(L, -2, \"__call\");\n\t}\n\tlua_setmetatable(L, -2);\n\t\n\treturn 1;\n}\n\nstatic int\nldraw(lua_State *L) {\n\tint base = luaL_checkinteger(L, 1);\n\tint n = luaL_checkinteger(L, 2);\n\tint inst = luaL_checkinteger(L, 3);\n\tsg_draw(base, n, inst);\n\treturn 0;\n}\n\nstatic int\nlsrbuffer_add(lua_State *L) {\n\tstruct sr_buffer *b = (struct sr_buffer *)luaL_checkudata(L, 1, \"SOLUNA_SRBUFFER\");\n\tfloat scale = luaL_checknumber(L, 2);\n\tfloat rot = luaL_checknumber(L, 3);\n\n\tstruct draw_primitive tmp;\n\tsprite_set_sr(&tmp, scale, rot);\n\tint index = srbuffer_add(b, tmp.sr);\n\tif (index < 0)\n\t\treturn 0;\n\tlua_pushinteger(L, index);\n\treturn 1;\n}\n\nstatic int\nlsrbuffer_ptr(lua_State *L) {\n\tstruct sr_buffer *b = (struct sr_buffer *)luaL_checkudata(L, 1, \"SOLUNA_SRBUFFER\");\n\tint sz;\n\tvoid * ptr = srbuffer_commit(b, &sz);\n\tif (ptr == NULL)\n\t\treturn 0;\n\tlua_pushlightuserdata(L, ptr);\n\tlua_pushinteger(L, sz);\n\treturn 2;\n}\n\nstatic int\nlsrbuffer(lua_State *L) {\n\tint n = luaL_checkinteger(L, 1);\n\tsize_t sz = srbuffer_size(n);\n\tstruct sr_buffer *b = (struct sr_buffer *)lua_newuserdatauv(L, sz, 0);\n\tsrbuffer_init(b, n);\n\tif (luaL_newmetatable(L, \"SOLUNA_SRBUFFER\")) {\n\t\tluaL_Reg l[] = {\n\t\t\t{ \"__index\", NULL },\n\t\t\t{ \"add\", lsrbuffer_add },\n\t\t\t{ \"ptr\", lsrbuffer_ptr },\n\t\t\t{ NULL, NULL },\n\t\t};\n\t\tluaL_setfuncs(L, l, 0);\n\n\t\tlua_pushvalue(L, -1);\n\t\tlua_setfield(L, -2, \"__index\");\n\t}\n\tlua_setmetatable(L, -2);\n\treturn 1;\n}\n\nstruct inst_object {\n\tfloat x, y;\n\tfloat sr_index;\n};\n\nstruct sprite_object {\n\tuint32_t off;\n\tuint32_t u;\n\tuint32_t v;\n};\n\nstatic int\nlbuffer_size(lua_State *L) {\n\tconst char * name = luaL_checkstring(L, 1);\n\tint n = luaL_optinteger(L, 2, 1);\n\tsize_t sz = 0;\n\tif (strcmp(name, \"srbuffer\") == 0) {\n\t\tsz = sizeof(struct sr_mat);\n\t} else if (strcmp(name, \"inst\") == 0) {\n\t\tsz = sizeof(struct inst_object);\n\t} else if (strcmp(name, \"sprite\") == 0) {\n\t\tsz = sizeof(struct sprite_object);\n\t} else {\n\t\treturn luaL_error(L, \"Invalid buffer type %s\", name);\n\t}\n\tlua_pushinteger(L, sz * n);\n\treturn 1;\n}\n\nstatic int\nltmp_buffer(lua_State *L) {\n\tsize_t sz = luaL_optinteger(L, 1, 128 * 1024);\n\tlua_newuserdatauv(L, sz, 0);\n\treturn 1;\n}\n\nint lbindings_new(lua_State *L);\nint lview_new(lua_State *L);\nint luniform_new(lua_State *L);\n\nint\nluaopen_render(lua_State *L) {\n\tluaL_checkversion(L);\n\tluaL_Reg l[] = {\n\t\t{ \"pass\", lpass_new },\n\t\t{ \"submit\", lsubmit },\n\t\t{ \"image\", limage },\n\t\t{ \"buffer\", lbuffer },\n\t\t{ \"sampler\", lsampler },\n\t\t{ \"draw\", ldraw },\n\t\t{ \"srbuffer\", lsrbuffer },\n\t\t{ \"buffer_size\", lbuffer_size },\n\t\t{ \"bindings\", lbindings_new },\n\t\t{ \"view\", lview_new },\n\t\t{ \"uniform\", luniform_new },\n\t\t{ \"tmp_buffer\", ltmp_buffer },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\treturn 1;\n}"
  },
  {
    "path": "src/render_bindings.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n#include <string.h>\n\n#include \"render_bindings.h\"\n\nstatic inline sg_bindings *\nget_bindings(lua_State *L) {\n\tstruct render_bindings *b = luaL_checkudata(L, 1, \"SOKOL_BINDINGS\");\n\treturn &b->bindings;\t\n}\n\nstatic int\nlbindings_set_base(lua_State *L) {\n\tstruct render_bindings *b = luaL_checkudata(L, 1, \"SOKOL_BINDINGS\");\n\tint base = luaL_checkinteger(L, 2);\n\tb->base = base;\n\treturn 0;\n}\n\nstatic int\nlbindings_set_vb(lua_State *L) {\n\tsg_bindings *b = get_bindings(L);\n\tint index = luaL_checkinteger(L, 2);\n\tif (index < 0 || index >= SG_MAX_VERTEXBUFFER_BINDSLOTS)\n\t\treturn luaL_error(L, \"Invalid vbuffer slot %d\", index);\n\tluaL_checkudata(L, 3, \"SOKOL_BUFFER\");\n\tlua_settop(L, 3);\n\tlua_pushlightuserdata(L, &b->vertex_buffers[index]);\n\tlua_call(L, 1, 0);\n\treturn 0;\n}\n\nstatic int\nlbindings_set_voff(lua_State *L) {\n\tsg_bindings *b = get_bindings(L);\n\tint index = luaL_checkinteger(L, 2);\n\tif (index < 0 || index >= SG_MAX_VERTEXBUFFER_BINDSLOTS)\n\t\treturn luaL_error(L, \"Invalid vbuffer slot %d\", index);\n\tb->vertex_buffer_offsets[index] = luaL_checkinteger(L, 3);\n\treturn 0;\n}\n\nstatic int\nlbindings_set_ib(lua_State *L) {\n\tsg_bindings *b = get_bindings(L);\n\tluaL_checkudata(L, 2, \"SOKOL_BUFFER\");\n\tlua_settop(L, 2);\n\tlua_pushlightuserdata(L, &b->index_buffer);\n\tlua_call(L, 1, 0);\n\treturn 0;\n}\n\nstatic int\nlbindings_set_ioff(lua_State *L) {\n\tsg_bindings *b = get_bindings(L);\n\tb->index_buffer_offset = luaL_checkinteger(L, 2);\n\treturn 0;\n}\n\nstruct view {\n\tsg_view view;\n\tint type;\n};\n\nstatic int\nlbindings_set_view(lua_State *L) {\n\tsg_bindings *b = get_bindings(L);\n\tint index = luaL_checkinteger(L, 2);\n\tif (index < 0 || index >= SG_MAX_VIEW_BINDSLOTS)\n\t\treturn luaL_error(L, \"Invalid view slot %d\", index);\n\tstruct view *v = luaL_checkudata(L, 3, \"SOKOL_VIEW\");\n\tb->views[index] = v->view;\n\treturn 0;\n}\n\nstatic int\nlbindings_set_sampler(lua_State *L) {\n\tsg_bindings *b = get_bindings(L);\n\tint index = luaL_checkinteger(L, 2);\n\tif (index < 0 || index >= SG_MAX_SAMPLER_BINDSLOTS)\n\t\treturn luaL_error(L, \"Invalid sampler slot %d\", index);\n\tluaL_checkudata(L, 3, \"SOKOL_SAMPLER\");\n\tlua_settop(L, 3);\n\tlua_pushlightuserdata(L, &b->samplers[index]);\n\tlua_call(L, 1, 0);\n\treturn 0;\n}\n\nstatic int\nlbindings_apply(lua_State *L) {\n\tsg_bindings *b = get_bindings(L);\n\tsg_apply_bindings(b);\n\treturn 0;\n}\n\n#define VIEW_TYPE_INVALID 0\n#define VIEW_TYPE_TEXTURE 1\n#define VIEW_TYPE_STORAGE 2\n#define VIEW_TYPE_COLOR_ATTACHMENT 3\n#define VIEW_TYPE_DEPTH_STENCIL_ATTACHMENT 4\n\nstatic inline const char *\nview_type_string(int type) {\n\tswitch (type) {\n\tcase VIEW_TYPE_TEXTURE : return \"texture\";\n\tcase VIEW_TYPE_STORAGE : return \"storage\";\n\tcase VIEW_TYPE_COLOR_ATTACHMENT : return \"color_attachment\";\n\tcase VIEW_TYPE_DEPTH_STENCIL_ATTACHMENT : return \"depth_stencil_attachment\";\n\tdefault : return \"invalid\";\n\t}\n}\n\nstatic inline void\ncheck_view_type(lua_State *L, struct view *v) {\n\tif (v->type != VIEW_TYPE_INVALID)\n\t\tluaL_error(L, \"Invalid multi type set : %s\", view_type_string(v->type));\n}\n\nstatic int\nlview_tostring(lua_State *L) {\n\tstruct view *v = (struct view *)lua_touserdata(L, 1);\n\tconst char *s = view_type_string(v->type);\n\tlua_pushexternalstring(L, s, strlen(s), NULL, NULL);\n\treturn 1;\n}\n\nstatic int\nlview_release(lua_State *L) {\n\tstruct view *v = (struct view *)lua_touserdata(L, 1);\n\tif (v->type != VIEW_TYPE_INVALID) {\n\t\tv->type = VIEW_TYPE_INVALID;\n\t\tsg_destroy_view(v->view);\n\t}\n\treturn 0;\n}\n\nstatic int\nlview_getptr(lua_State *L) {\n\tstruct view *v = (struct view *)lua_touserdata(L, 1);\n\tif (v->type == VIEW_TYPE_INVALID) {\n\t\treturn 0;\n\t}\n\tlua_pushlightuserdata(L, &v->view);\n\treturn 1;\n}\n\nint\nlview_new(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TTABLE);\n\tstruct sg_view_desc desc;\n\tmemset(&desc, 0 , sizeof(desc));\n\tstruct view *v = NULL;\n\tif (lua_getfield(L, 1, \"label\") == LUA_TSTRING) {\n\t\tv = (struct view *)lua_newuserdatauv(L, sizeof(*v), 1);\n\t\tlua_insert(L, -2);\n\t\tdesc.label = lua_tostring(L, -1);\n\t\tlua_setiuservalue(L, 1, 1);\n\t} else {\n\t\tlua_pop(L, 1);\n\t\tv = (struct view *)lua_newuserdatauv(L, sizeof(*v), 0);\n\t}\n\tv->type = VIEW_TYPE_INVALID;\n\tif (lua_getfield(L, 1, \"texture\") == LUA_TUSERDATA) {\n\t\tcheck_view_type(L, v);\n\t\tv->type = VIEW_TYPE_TEXTURE;\n\t\t// image\n\t\tluaL_checkudata(L, -1, \"SOKOL_IMAGE\");\n\t\tlua_pushlightuserdata(L, &desc.texture.image);\n\t\tlua_call(L, 1, 0);\n\t} else {\n\t\tlua_pop(L, 1);\n\t}\n\tif (lua_getfield(L, 1, \"storage\") == LUA_TUSERDATA) {\n\t\tcheck_view_type(L, v);\n\t\tv->type = VIEW_TYPE_STORAGE;\n\t\tluaL_checkudata(L, -1, \"SOKOL_BUFFER\");\n\t\tlua_pushlightuserdata(L, &desc.storage_buffer.buffer);\n\t\tlua_call(L, 1, 0);\n\t} else {\n\t\tlua_pop(L, 1);\n\t}\n\tif (lua_getfield(L, 1, \"color_attachment\") == LUA_TUSERDATA) {\n\t\tcheck_view_type(L, v);\n\t\tv->type = VIEW_TYPE_COLOR_ATTACHMENT;\n\t\tluaL_checkudata(L, -1, \"SOKOL_IMAGE\");\n\t\tlua_pushlightuserdata(L, &desc.color_attachment.image);\n\t\tlua_call(L, 1, 0);\n\t} else {\n\t\tlua_pop(L, 1);\n\t}\n\tif (lua_getfield(L, 1, \"depth_stencil_attachment\") == LUA_TUSERDATA) {\n\t\tcheck_view_type(L, v);\n\t\tv->type = VIEW_TYPE_DEPTH_STENCIL_ATTACHMENT;\n\t\tluaL_checkudata(L, -1, \"SOKOL_IMAGE\");\n\t\tlua_pushlightuserdata(L, &desc.depth_stencil_attachment.image);\n\t\tlua_call(L, 1, 0);\n\t} else {\n\t\tlua_pop(L, 1);\n\t}\n\tif (v->type == VIEW_TYPE_INVALID)\n\t\treturn luaL_error(L, \"No view type\");\n\tv->view = sg_make_view(&desc);\n\tif (luaL_newmetatable(L, \"SOKOL_VIEW\")) {\n\t\tluaL_Reg l[] = {\n\t\t\t{ \"__index\", NULL },\n\t\t\t{ \"__tostring\", lview_tostring },\n\t\t\t{ \"__gc\", lview_release },\n\t\t\t{ \"__call\", lview_getptr },\n\t\t\t{ NULL, NULL },\n\t\t};\n\t\tluaL_setfuncs(L, l, 0);\n\n\t\tlua_pushvalue(L, -1);\n\t\tlua_setfield(L, -2, \"__index\");\n\t}\n\tlua_setmetatable(L, -2);\n\treturn 1;\n}\n\nint\nlbindings_new(lua_State *L) {\n\tstruct render_bindings *b = (struct render_bindings *)lua_newuserdatauv(L, sizeof(*b), 0);\n\tmemset(b, 0, sizeof(*b));\n\tif (luaL_newmetatable(L, \"SOKOL_BINDINGS\")) {\n\t\tluaL_Reg l[] = {\n\t\t\t{ \"__index\", NULL },\n\t\t\t{ \"base\", lbindings_set_base },\n\t\t\t{ \"vbuffer\", lbindings_set_vb },\n\t\t\t{ \"voffset\", lbindings_set_voff },\n\t\t\t{ \"ibuffer\", lbindings_set_ib },\n\t\t\t{ \"ioffset\", lbindings_set_ioff },\n\t\t\t{ \"view\", lbindings_set_view },\n\t\t\t{ \"sampler\", lbindings_set_sampler },\n\t\t\t{ \"apply\", lbindings_apply },\n\t\t\t{ NULL, NULL },\n\t\t};\n\t\tluaL_setfuncs(L, l, 0);\n\n\t\tlua_pushvalue(L, -1);\n\t\tlua_setfield(L, -2, \"__index\");\n\t}\n\tlua_setmetatable(L, -2);\n\treturn 1;\n}\n"
  },
  {
    "path": "src/render_bindings.h",
    "content": "#ifndef soluna_render_bindings_h\n#define soluna_render_bindings_h\n\n#include \"sokol/sokol_gfx.h\"\n\nstruct render_bindings {\n\tint base;\n\tsg_bindings bindings;\n};\n\n#define DRAWFUNC(name) (sg_query_features().draw_base_instance ? name##_ex : name)\n\n#endif\n"
  },
  {
    "path": "src/render_uniform.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n#include <string.h>\n\n#include \"sokol/sokol_gfx.h\"\n\n#define UNIFORM_TYPE_FLOAT 1\n#define UNIFORM_TYPE_INT 2\n\nstatic void\nset_uniform_float(lua_State *L, int index, uint8_t *buffer, int offset, int n) {\n\tif (n <= 1) {\n\t\tfloat v = luaL_checknumber(L, index);\n\t\tmemcpy(buffer + offset, &v, sizeof(float));\n\t} else {\n\t\tluaL_checktype(L, index, LUA_TTABLE);\n\t\tif (lua_rawlen(L, index) != n) {\n\t\t\tluaL_error(L, \"Need table size %d\", n);\n\t\t}\n\t\tint i;\n\t\tfloat * v = (float *)(buffer + offset);\n\t\tfor (i=0;i<n;i++) {\n\t\t\tif (lua_rawgeti(L, index, i+1) != LUA_TNUMBER) {\n\t\t\t\tluaL_error(L, \"Invalid value in table[%d]\", i+1);\n\t\t\t}\n\t\t\tfloat tmp = lua_tonumber(L, -1);\n\t\t\tmemcpy(v, &tmp, sizeof(float));\n\t\t\tv++;\n\t\t\tlua_pop(L, 1);\n\t\t}\n\t}\n}\n\nstatic void\nset_uniform_int(lua_State *L, int index, uint8_t *buffer, int offset, int n) {\n\tif (n <= 1) {\n\t\tint v = luaL_checkinteger(L, index);\n\t\tmemcpy(buffer + offset, &v, sizeof(int));\n\t} else {\n\t\tluaL_checktype(L, index, LUA_TTABLE);\n\t\tif (lua_rawlen(L, index) != n) {\n\t\t\tluaL_error(L, \"Need table size %d\", n);\n\t\t}\n\t\tint i;\n\t\tint *v = (int *)(buffer + offset);\n\t\tfor (i=0;i<n;i++) {\n\t\t\tif (lua_rawgeti(L, index, i+1) != LUA_TNUMBER) {\n\t\t\t\tluaL_error(L, \"Invalid value in table[%d]\", i+1);\n\t\t\t}\n\t\t\tint tmp = lua_tointeger(L, -1);\n\t\t\tmemcpy(v, &tmp, sizeof(int));\n\t\t\t++v;\n\t\t\tlua_pop(L, 1);\n\t\t}\n\t}\n}\n\nstatic int\nluniform_set(lua_State *L) {\n\tuint8_t * p = (uint8_t *)lua_touserdata(L, 1);\n\tif (p == NULL || lua_getiuservalue(L, 1, 1) != LUA_TTABLE)\n\t\treturn luaL_error(L, \"Invalid uniform setter, call init first\");\n\tlua_pushvalue(L, 2);\t// key\n\tif (lua_rawget(L, -2) != LUA_TNUMBER)\n\t\treturn luaL_error(L, \"No uniform %s\", lua_tostring(L, 2));\n\t// ud key value desc meta\n\tuint32_t meta = lua_tointeger(L, -1);\n\tlua_pop(L, 2);\n\tint type = meta >> 28;\n\tint offset = (meta >> 14) & 0x3fff;\n\tint n = meta & 0x3fff;\n\tswitch (type) {\n\tcase UNIFORM_TYPE_FLOAT:\n\t\tset_uniform_float(L, 3, p, offset, n);\n\t\tbreak;\n\tcase UNIFORM_TYPE_INT:\n\t\tset_uniform_int(L, 3, p, offset, n);\n\t\tbreak;\n\tdefault:\n\t\treturn luaL_error(L, \"Invalid uniform setter typeid %s (%d)\", lua_tostring(L, 2), type);\n\t}\n\treturn 0;\n}\n\nstatic void\nset_key(lua_State *L, int index, uint8_t *u, int size) {\n\tconst char * key = lua_tostring(L, -2);\n\tif (lua_getfield(L, -1, \"offset\") != LUA_TNUMBER) {\n\t\tluaL_error(L, \"Missing .%s .offset\", key);\n\t}\n\tint offset = luaL_checkinteger(L, -1);\n\tlua_pop(L, 1);\n\tif (offset < 0 || offset > 0x3fff)\n\t\tluaL_error(L, \"Invalid .%s offset = %d\", key, offset);\n\t\n\tint t = lua_getfield(L, -1, \"n\");\n\tint n = 1;\n\tif (t != LUA_TNUMBER && t != LUA_TNIL) {\n\t\tluaL_error(L, \"Invalid type .%s .n (%s)\", key, lua_typename(L, t));\n\t}\n\tif (t == LUA_TNUMBER) {\n\t\tn = luaL_checkinteger(L, -1);\n\t}\n\tif (n < 1 || n > 0x3fff)\n\t\tluaL_error(L, \"Invalid .%s n = %d\", key, n);\n\tlua_pop(L, 1);\n\tif (lua_getfield(L, -1, \"type\") != LUA_TSTRING) {\n\t\tluaL_error(L, \"Missing .%s .type\", key);\n\t}\n\tconst char *type = lua_tostring(L, -1);\n\tint tid = 0;\n\tint typesize = 0;\n\tif (strcmp(type, \"float\") == 0) {\n\t\ttid = UNIFORM_TYPE_FLOAT;\n\t\ttypesize = sizeof(float);\n\t} else if (strcmp(type, \"int\") == 0) {\n\t\ttid = UNIFORM_TYPE_INT;\n\t\ttypesize = sizeof(int);\n\t} else {\n\t\tluaL_error(L, \"Invalid .%s .type %s\", key, type);\n\t}\n\tlua_pop(L, 1);\n\tint offset_end = offset + n * typesize;\n\tif (offset_end > size)\n\t\tluaL_error(L, \"Invalid uniform .%s (offset %d,n %d)\", key, offset, n);\n\tint i;\n\tfor (i=offset;i<offset_end;i++) {\n\t\tif (u[i] != 0)\n\t\t\tluaL_error(L, \"Overlap uniform .%s (offset %d,n %d)\", key, offset, n);\n\t\tu[i] = 1;\n\t}\n\tuint32_t info = (tid << 28) | (offset << 14) | n;\n\tlua_pushvalue(L, -2);\n\tlua_pushinteger(L, info);\n\tlua_rawset(L, index);\n}\n\nstatic void\ncheck_uniform(lua_State *L, uint8_t *buffer, int size) {\n\tint i;\n\tfor (i=0;i<size;i++) {\n\t\tif (buffer[i] == 0)\n\t\t\tluaL_error(L, \"Uniform is not complete\");\n\t}\n}\n\nstatic void\nget_typeinfo(lua_State *L, int index, uint8_t *u, int size) {\n\tlua_newtable(L);\t// typeinfo for uniform\n\tint typeinfo = lua_gettop(L);\n\t\n\t// iter desc table 2\n\tlua_pushnil(L);\n\twhile (lua_next(L, index) != 0) {\n\t\tint t = lua_type(L, -2);\n\t\tif (t != LUA_TSTRING) {\n\t\t\tif (t == LUA_TNUMBER && lua_tointeger(L, -2) == 1) {\n\t\t\t\t// size of uniform\n\t\t\t\tlua_pop(L, 1);\n\t\t\t} else {\n\t\t\t\tluaL_error(L, \"Init error : none string key\");\n\t\t\t}\n\t\t}\n\t\telse if (lua_type(L, -1) != LUA_TTABLE) {\n\t\t\tluaL_error(L, \"Init error : invalid .%s\", lua_tostring(L, -2));\n\t\t} else {\n\t\t\tset_key(L, typeinfo, u, size);\n\t\t\tlua_pop(L, 1);\n\t\t}\n\t}\n\tcheck_uniform(L, u, size);\n\tmemset(u, 0, size);\n}\n\nstatic int\nluniform_apply(lua_State *L) {\n\tuint8_t * buffer = (uint8_t *)luaL_checkudata(L, 1, \"SOKOL_UNIFORM\");\n\tint size = lua_rawlen(L, 1);\n\tint slot = luaL_checkinteger(L, 2);\n\tsg_apply_uniforms(slot, &(sg_range){ buffer, size });\n\treturn 0;\n}\n\nint\nluniform_new(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TTABLE);\n\tif (lua_geti(L, 1, 1) != LUA_TNUMBER) {\n\t\treturn luaL_error(L, \"Invalid ._size of uniform\");\n\t}\n\tint size = lua_tointeger(L, -1);\n\tlua_pop(L, 1);\n//\tif (size <= 0 || size % 16 != 0) {\n//\t\treturn luaL_error(L, \"Invalid the _size (%d) of uniform should align to 16\", size);\n//\t}\n\tint align_size = (size + 15) / 16 * 16;\n\tvoid * u = lua_newuserdatauv(L, align_size, 1);\n\tmemset(u, 0, align_size);\n\n\tget_typeinfo(L, 1, u, size);\n\tlua_setiuservalue(L, -2, 1);\n\n\tif (luaL_newmetatable(L, \"SOKOL_UNIFORM\")) {\n\t\tluaL_Reg l[] = {\n\t\t\t{ \"__index\", NULL },\n\t\t\t{ \"__newindex\", luniform_set },\n\t\t\t{ \"apply\", luniform_apply },\n\t\t\t{ NULL, NULL },\n\t\t};\n\t\tluaL_setfuncs(L, l, 0);\n\n\t\tlua_pushvalue(L, -1);\n\t\tlua_setfield(L, -2, \"__index\");\n\t}\n\tlua_setmetatable(L, -2);\n\treturn 1;\n}\n"
  },
  {
    "path": "src/sdfimage.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n#include <math.h>\n#include <string.h>\n\n#include \"font_define.h\"\n\n// implement from image.c\n#include \"stb/stb_image.h\"\n\n#define STB_IMAGE_RESIZE_IMPLEMENTATION\n#include \"stb/stb_image_resize2.h\"\n\n#include \"stb/stb_image_write.h\"\n\n#include \"luabuffer.h\"\n\n#define MAX_SIZE 4096\n#define INF 1e20\n#define SDF_RADIUS 8\n#define SDF_CUTOFF 0.25\n// the same with font glyph size\n#define IMAGE_SIZE 64\n\n// 1D squared distance transform\nstatic void\nedt1d(const double *f, double *d, int *v, double *z, int n) {\n\tint q,k;\n\t\n\tv[0] = 0;\n\tz[0] = -INF;\n\tz[1] = +INF;\n\n\tfor (q = 1, k = 0; q < n; q++) {\n\t\tdouble s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]);\n\t\twhile (s <= z[k]) {\n\t\t\tk--;\n\t\t\ts = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]);\n\t\t}\n\t\tk++;\n\t\tv[k] = q;\n\t\tz[k] = s;\n\t\tz[k + 1] = +INF;\n\t}\n\n\tfor (q = 0, k = 0; q < n; q++) {\n        while (z[k + 1] < q)\n\t\t\tk++;\n\t\td[q] = (q - v[k]) * (q - v[k]) + f[v[k]];\n    }\n}\n\n// 2D Euclidean distance transform by Felzenszwalb & Huttenlocher https://cs.brown.edu/~pff/dt/\nstatic void\nedt(double *data, int width, int height, double *f, double *d, int *v, double *z) {\n\tint x,y;\n\tfor (x = 0; x < width; x++) {\n\t\tfor (y = 0; y < height; y++) {\n\t\t\tf[y] = data[y * width + x];\n\t\t}\n\t\tedt1d(f, d, v, z, height);\n\t\tfor (y = 0; y < height; y++) {\n\t\t\tdata[y * width + x] = d[y];\n\t\t}\n    }\n\tfor (y = 0; y < height; y++) {\n\t\tfor (x = 0; x < width; x++) {\n\t\t\tf[x] = data[y * width + x];\n\t\t}\n\t\tedt1d(f, d, v, z, width);\n\t\tfor (x = 0; x < width; x++) {\n\t\t\tdata[y * width + x] = sqrt(d[x]);\n\t\t}\n\t}\n}\n\nstruct sdf_context {\n\tdouble f[MAX_SIZE];\n\tdouble d[MAX_SIZE];\n\tdouble z[MAX_SIZE+1];\n\tint v[MAX_SIZE];\n};\n\nstatic void\nsdf_convert(unsigned char *bytes, int x, int y, double radius, double cutoff) {\n\tint i;\n\tassert(x <= MAX_SIZE && y <= MAX_SIZE);\n\tint length = x * y;\n\tdouble *data = (double *)malloc(length * sizeof(double) * 3 + sizeof(struct sdf_context));\n\tfor (i = 0; i < length; i++) {\n\t\t// For white background, negative image\n\t\tdata[i] = (255 - bytes[i]) / 255.0;\n\t}\n\tdouble *gridOuter = data + length;\n\tdouble *gridInner = gridOuter + length;\n\tstruct sdf_context * ctx = (struct sdf_context *)(gridInner + length);\n\n\tfor (i = 0; i < length; i++) {\n\t\tdouble a = data[i];\n\t\tif (a >= 0.5) {\n\t\t\tgridOuter[i] = 0;\n\t\t\tif (a >= 1) {\n\t\t\t\tgridInner[i] = INF;\n\t\t\t} else {\n\t\t\t\ta -= 0.5;\n\t\t\t\tgridInner[i] = a * a;\n\t\t\t}\n\t\t} else if (a <= 0.5) {\n\t\t\tgridInner[i] = 0;\n\t\t\tif (a <= 0) {\n\t\t\t\tgridOuter[i] = INF;\n\t\t\t} else {\n\t\t\t\ta = 0.5 - a;\n\t\t\t\tgridOuter[i] = a * a;\n\t\t\t}\n\t\t}\n\t}\n\n    edt(gridOuter, x, y, ctx->f, ctx->d, ctx->v, ctx->z);\n    edt(gridInner, x, y, ctx->f, ctx->d, ctx->v, ctx->z);\n\n\tfor (i = 0; i < length; i++) {\n\t\tdouble v = (gridOuter[i] - gridInner[i]) / radius + cutoff;\n\t\tif (v <= 0) {\n\t\t\tbytes[i] = 255;\n\t\t} else if (v >= 1) {\n\t\t\tbytes[i] = 0;\n\t\t} else {\n\t\t\tunsigned char byte = (unsigned char)(v * 255.0 + 0.5);\n\t\t\tbytes[i] = 255 - byte;\n\t\t}\n    }\n\n\tfree(data);\n}\n\nstatic void *\nfree_image(void *ud, void *ptr, size_t osize, size_t nsize) {\n\tstbi_image_free(ptr);\n\treturn NULL;\n}\n\nstatic void *\nfree_resizeimage(void *ud, void *ptr, size_t osize, size_t nsize) {\n\tfree(ptr);\n\treturn NULL;\n}\n\nstatic int\nimage_loadsdf(lua_State *L) {\n\tsize_t sz;\n\tconst stbi_uc *buffer = luaL_getbuffer(L, &sz);\n\tint x,y;\n\tstbi_uc * img = stbi_load_from_memory(buffer, sz, &x, &y, NULL, 1);\n\tif (img == NULL) {\n\t\tlua_pushnil(L);\n\t\tlua_pushstring(L, stbi_failure_reason());\n\t\treturn 2;\n\t}\n\t\n\tint target_w = luaL_optinteger(L, 2, IMAGE_SIZE);\n\tint target_h = luaL_optinteger(L, 3, target_w);\n\tif (target_w <= 0 || target_w > MAX_SIZE || target_h <= 0 || target_w > MAX_SIZE) {\n\t\treturn luaL_error(L, \"Invalid target size %d * %d\", target_w, target_h);\n\t}\n\n\tdouble radius = (double)x * SDF_RADIUS / target_w;\n\tsdf_convert(img, x, y, radius, SDF_CUTOFF);\n\t\n\tif (x == target_w && y == target_h) {\n\t\tlua_pushexternalstring(L, (const char *)img, x * y, free_image, NULL);\n\t} else {\n\t\tsize_t size = target_w * target_h;\n\t\tunsigned char * target = (unsigned char *)malloc(size + 1);\n\t\tif (target == NULL) {\n\t\t\tstbi_image_free(img);\n\t\t\treturn luaL_error(L, \"Out of memory for image\");\n\t\t}\n\t\ttarget[size] = 0;\n\t\tstbir_resize_uint8_linear(img , x , y, x,\n\t\t\ttarget, target_w, target_h, target_w, STBIR_1CHANNEL);\n\t\tstbi_image_free(img);\n\t\tlua_pushexternalstring(L, (const char *)target, size, free_resizeimage, NULL);\n\t}\n\n\treturn 1;\n}\n\nstatic int\nimage_savesdf(lua_State *L) {\n\tconst char * filename = luaL_checkstring(L, 1);\n\tsize_t sz;\n\tconst char * buffer = luaL_checklstring(L, 2, &sz);\n\tint x = luaL_optinteger(L, 3, IMAGE_SIZE);\n\tint y = luaL_optinteger(L, 4, x);\n\tif (x * y != sz) {\n\t\treturn luaL_error(L, \"Invalid image size %d * %d\", x, y);\n\t}\n\tif (!stbi_write_png(filename, x, y, 1, buffer, x)) {\n\t\treturn luaL_error(L, \"Write %s failed\", filename);\n\t}\n\treturn 1;\n}\n\nstatic int\nicon_bundle(lua_State *L) {\n\tluaL_checktype(L, 1, LUA_TTABLE);\n\tint n = lua_rawlen(L, 1);\n\tsize_t sz = n * FONT_MANAGER_GLYPHSIZE * FONT_MANAGER_GLYPHSIZE;\n\tvoid * buffer = lua_newuserdatauv(L, sz, 0);\n\tint i;\n\tfor (i=0; i<n; i++) {\n\t\t// import icon i\n\t\tif (lua_geti(L, 1, i+1) != LUA_TSTRING) {\n\t\t\treturn luaL_error(L, \"Invalid icon %d (%s)\", i+1, lua_typename(L, lua_type(L, -1)));\n\t\t}\n\t\tsize_t icon_size;\n\t\tconst char *icon = lua_tolstring(L, -1, &icon_size);\n\t\tif (icon_size != FONT_MANAGER_GLYPHSIZE * FONT_MANAGER_GLYPHSIZE) {\n\t\t\treturn luaL_error(L, \"Invalid icon %d size, need %d * %d bytes\", i+1, FONT_MANAGER_GLYPHSIZE, FONT_MANAGER_GLYPHSIZE);\n\t\t}\n\t\tunsigned char *ptr = (unsigned char *)buffer + i * FONT_MANAGER_GLYPHSIZE * FONT_MANAGER_GLYPHSIZE;\n\t\tmemcpy(ptr, icon, FONT_MANAGER_GLYPHSIZE * FONT_MANAGER_GLYPHSIZE);\n\t\tlua_pop(L, 1);\n\t}\n\treturn 1;\n}\n\nint\nluaopen_image_sdf(lua_State *L) {\n\tluaL_checkversion(L);\n\tluaL_Reg l[] = {\n\t\t{ \"bundle\", icon_bundle },\n\t\t{ \"load\", image_loadsdf },\n\t\t{ \"save\", image_savesdf },\t// for debug\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\treturn 1;\n}\n\n"
  },
  {
    "path": "src/sdftext.glsl",
    "content": "@vs vs\nlayout(binding=0) uniform vs_params {\n\tvec2 framesize;\n\tfloat texsize;\n};\n\nstruct sr_mat {\n\tmat2 m;\n};\n\nlayout(binding=0) readonly buffer sr_lut {\n\tsr_mat sr[];\n};\n\nin vec3 position;\nin uint offset;\nin uint u;\nin uint v;\n\nout vec2 uv;\n\nvoid main() {\n\tivec2 uv_base = ivec2(u >> 16, v >> 16);\n\tivec2 u2 = ivec2(0 , u & 0xffff);\n\tivec2 v2 = ivec2(0 , v & 0xffff);\n\tivec2 off = ivec2(offset >> 16 , offset & 0xffff) - 0x8000;\n\tvec2 uv_offset = vec2(u2[gl_VertexIndex & 1] , v2[gl_VertexIndex >> 1]);\n\tvec2 pos = ((uv_offset - off) * sr[int(position.z)].m + position.xy) * framesize;\n\tgl_Position = vec4(pos.x - 1.0f, pos.y + 1.0f, 0, 1);\n\tuv = (uv_base + uv_offset) * texsize;\n}\n\n@end\n\n@fs fs\nlayout(binding=1) uniform texture2D tex;\nlayout(binding=0) uniform sampler smp;\n\nlayout(binding=1) uniform fs_params {\n\tfloat edge_mask;\n\tfloat dist_multiplier;\n\tint color;\n\tfloat unused;\n};\n\nin vec2 uv;\nout vec4 frag_color;\n\nvoid main() {\n\tfloat dis = texture(sampler2D(tex,smp), uv).r;\n\tfloat smoothing = length(fwidth(uv)) * 128.0 * dist_multiplier;\n\tfloat alpha = smoothstep(edge_mask - smoothing, edge_mask + smoothing, dis);\n\tfloat color_alpha = ((color >> 24) & 0xff) / 255.0f;\n\tvec4 c = vec4(\n\t\t((color >> 16) & 0xff) / 255.0f,\n\t\t((color >> 8) & 0xff) / 255.0f,\n\t\t((color) & 0xff) / 255.0f,\n\t\tcolor_alpha * alpha);\n\tfrag_color = c;\n}\n@end\n\n@program texquad vs fs"
  },
  {
    "path": "src/service/audio.lua",
    "content": "local ltask = require \"ltask\"\nlocal audio = require \"soluna.audio\"\nlocal file = require \"soluna.file\"\nlocal datalist = require \"soluna.datalist\"\n\nglobal assert, error, ipairs, math, pairs, tonumber, tostring\n\nlocal device\nlocal definitions = {}\nlocal groups = {}\nlocal voices = {}\nlocal bundles = {}\nlocal next_voice_id_value = 0\nlocal ziplist\nlocal is_quit\n\nlocal SOUND_FLAG_STREAM = 0x00000001\nlocal DEFAULT_DEFINITION = {\n\tgroup = \"sound\",\n\tvolume = 1.0,\n\tpan = 0.0,\n\tpitch = 1.0,\n\tloop = false,\n\tstream = false,\n}\n\nlocal function convert_value(v)\n\tif v == \"true\" then\n\t\treturn true\n\tend\n\tif v == \"false\" then\n\t\treturn false\n\tend\n\treturn tonumber(v) or v\nend\n\nlocal function load_bundle(filename)\n\tlocal source = file.load(filename)\n\tlocal bundle = datalist.parse(assert(source, \"Can't load audio bundle \" .. tostring(filename)))\n\tlocal defs = {}\n\tfor _, v in ipairs(bundle) do\n\t\tlocal def = {\n\t\t\tgroup = DEFAULT_DEFINITION.group,\n\t\t\tvolume = DEFAULT_DEFINITION.volume,\n\t\t\tpan = DEFAULT_DEFINITION.pan,\n\t\t\tpitch = DEFAULT_DEFINITION.pitch,\n\t\t\tloop = DEFAULT_DEFINITION.loop,\n\t\t\tstream = DEFAULT_DEFINITION.stream,\n\t\t}\n\t\tfor k, value in pairs(v) do\n\t\t\tdef[k] = convert_value(value)\n\t\tend\n\t\tdef.name = assert(def.name)\n\t\tdef.filename = assert(def.filename)\n\t\tif defs[def.name] then\n\t\t\terror(\"Duplicate sound \" .. tostring(def.name))\n\t\tend\n\t\tdefs[def.name] = def\n\tend\n\treturn defs\nend\n\nlocal function merge_definition(def, opts)\n\tif opts == nil then\n\t\treturn {\n\t\t\tfilename = def.filename,\n\t\t\tgroup = def.group,\n\t\t\tvolume = def.volume,\n\t\t\tpan = def.pan,\n\t\t\tpitch = def.pitch,\n\t\t\tloop = def.loop,\n\t\t\tstream = def.stream,\n\t\t}\n\tend\n\tlocal loop = opts.loop\n\tif loop == nil then\n\t\tloop = def.loop\n\tend\n\tlocal stream = opts.stream\n\tif stream == nil then\n\t\tstream = def.stream\n\tend\n\treturn {\n\t\tfilename = def.filename,\n\t\tgroup = opts.group ~= nil and opts.group or def.group,\n\t\tvolume = opts.volume ~= nil and opts.volume or def.volume,\n\t\tpan = opts.pan ~= nil and opts.pan or def.pan,\n\t\tpitch = opts.pitch ~= nil and opts.pitch or def.pitch,\n\t\tloop = loop,\n\t\tstream = stream,\n\t}\nend\n\nlocal function release_voice(id)\n\tlocal voice = voices[id]\n\tif not voice then\n\t\treturn\n\tend\n\taudio.sound_uninit(voice)\n\tvoices[id] = nil\nend\n\nlocal function cleanup_voices()\n\tfor id, voice in pairs(voices) do\n\t\tif not audio.sound_playing(voice) then\n\t\t\trelease_voice(id)\n\t\tend\n\tend\nend\n\nlocal function next_voice_id()\n\tnext_voice_id_value = next_voice_id_value + 1\n\treturn next_voice_id_value\nend\n\nlocal function seconds_to_ms(seconds)\n\tif seconds == nil then\n\t\treturn nil\n\tend\n\tif seconds <= 0 then\n\t\treturn 0\n\tend\n\treturn math.floor(seconds * 1000 + 0.5)\nend\n\nltask.fork(function()\n\twhile not is_quit do\n\t\tcleanup_voices()\n\t\tltask.sleep(20)\n\tend\nend)\n\nlocal S = {}\n\nfunction S.init_device(dev)\n\tziplist = file.ziplist and file.ziplist()\n\tif ziplist then\n\t\taudio.init_vfs(dev, ziplist)\n\tend\n\tdevice = dev\nend\n\nfunction S.init(filename)\n\tif bundles[filename] then\n\t\treturn\n\tend\n\tlocal defs = load_bundle(filename)\n\tfor name, def in pairs(defs) do\n\t\tif definitions[name] then\n\t\t\terror(\"Duplicate sound \" .. tostring(name))\n\t\tend\n\t\tlocal group = groups[def.group]\n\t\tif group == nil then\n\t\t\tgroup = assert(audio.group_init(device))\n\t\t\tgroups[def.group] = group\n\t\tend\n\t\tdefinitions[name] = def\n\tend\n\tbundles[filename] = true\nend\n\nfunction S.play_sound(name, opts)\n\tlocal def = definitions[name]\n\tif def == nil then\n\t\treturn nil, \"Unknown sound \" .. tostring(name)\n\tend\n\n\tlocal final = merge_definition(def, opts)\n\tlocal group = groups[final.group]\n\tif group == nil then\n\t\treturn nil, \"Unknown audio bus \" .. tostring(final.group)\n\tend\n\n\tlocal flags = final.stream and SOUND_FLAG_STREAM or 0\n\n\tlocal voice, err = audio.sound_init(device, final.filename, flags, group)\n\tif not voice then\n\t\treturn nil, err\n\tend\n\n\taudio.sound_set_volume(voice, final.volume)\n\taudio.sound_set_pan(voice, final.pan)\n\taudio.sound_set_pitch(voice, final.pitch)\n\taudio.sound_set_looping(voice, final.loop == true)\n\n\tlocal ok, start_err = audio.sound_start(voice)\n\tif not ok then\n\t\taudio.sound_uninit(voice)\n\t\treturn nil, start_err\n\tend\n\n\tlocal id = next_voice_id()\n\tvoices[id] = voice\n\treturn id\nend\n\nfunction S.has_bus(name)\n\treturn groups[name] ~= nil\nend\n\nfunction S.voice_stop(id, fade_seconds)\n\tlocal voice = voices[id]\n\tif not voice then\n\t\treturn false\n\tend\n\treturn audio.sound_stop(voice, seconds_to_ms(fade_seconds)) ~= nil\nend\n\nfunction S.voice_playing(id)\n\tlocal voice = voices[id]\n\tif not voice then\n\t\treturn false\n\tend\n\tlocal playing = audio.sound_playing(voice)\n\tif not playing then\n\t\trelease_voice(id)\n\tend\n\treturn playing\nend\n\nfunction S.voice_set_volume(id, volume)\n\tlocal voice = voices[id]\n\tif not voice then\n\t\treturn false\n\tend\n\taudio.sound_set_volume(voice, volume)\n\treturn true\nend\n\nfunction S.voice_set_pan(id, pan)\n\tlocal voice = voices[id]\n\tif not voice then\n\t\treturn false\n\tend\n\taudio.sound_set_pan(voice, pan)\n\treturn true\nend\n\nfunction S.voice_set_pitch(id, pitch)\n\tlocal voice = voices[id]\n\tif not voice then\n\t\treturn false\n\tend\n\taudio.sound_set_pitch(voice, pitch)\n\treturn true\nend\n\nfunction S.voice_set_loop(id, loop)\n\tlocal voice = voices[id]\n\tif not voice then\n\t\treturn false\n\tend\n\taudio.sound_set_looping(voice, loop)\n\treturn true\nend\n\nfunction S.voice_seek(id, seconds)\n\tlocal voice = voices[id]\n\tif not voice then\n\t\treturn false\n\tend\n\treturn audio.sound_seek(voice, seconds) ~= nil\nend\n\nfunction S.voice_tell(id)\n\tlocal voice = voices[id]\n\tif not voice then\n\t\treturn nil, \"Voice not found\"\n\tend\n\treturn audio.sound_tell(voice)\nend\n\nfunction S.bus_set_volume(name, volume)\n\tlocal group = groups[name]\n\tif group == nil then\n\t\treturn false\n\tend\n\taudio.group_set_volume(group, volume)\n\treturn true\nend\n\nfunction S.quit()\n\tis_quit = true\n\tfor id in pairs(voices) do\n\t\trelease_voice(id)\n\tend\n\tfor name, group in pairs(groups) do\n\t\taudio.group_uninit(group)\n\t\tgroups[name] = nil\n\tend\n\tdevice = nil\n\tdefinitions = {}\n\tbundles = {}\nend\n\nreturn S\n"
  },
  {
    "path": "src/service/gamepad.lua",
    "content": "local ltask = require \"ltask\"\nlocal device = require \"soluna.gamepad.device\"\n\nglobal pairs\n\nlocal S = {}\n\nlocal listener = {}\n\nfunction S.register(addr, msg)\n\tlistener[addr] = msg\n\tltask.send(addr, msg)\nend\n\nfunction S.update()\n\tlocal change = device.update()\n\tif change then\n\t\tfor addr, msg in pairs(listener) do\n\t\t\tltask.send(addr, msg)\n\t\tend\n\tend\nend\n\nreturn S\n"
  },
  {
    "path": "src/service/loader.lua",
    "content": "local image = require \"soluna.image\"\nlocal spritemgr = require \"soluna.spritemgr\"\nlocal spritebundle = require \"soluna.spritebundle\"\n\nglobal setmetatable, ipairs, pairs, assert, type\n\nlocal sprite_bank\n\n-- todo: make weak table\nlocal filecache = setmetatable({ __missing = {}} , { __index = spritebundle.loadimage })\n\nlocal S = {}\n\nfunction S.init(config)\n\tsprite_bank = spritemgr.newbank(config.max_sprite, config.texture_size)\n\treturn sprite_bank:ptr()\nend\n\nlocal bundle = {}\nlocal sprite = {}\n\nlocal function add_list(desc)\n\tlocal b = {}\n\tfor _, item in ipairs(desc) do\n\t\tlocal n = #item\n\t\tif n == 0 then\n\t\t\tlocal id = sprite_bank:add(item.cw, item.ch, item.x, item.y)\n\t\t\titem.id = id\n\t\t\tsprite[id] = item\n\t\t\tb[item.name] = id\n\t\telse\n\t\t\tlocal pack = {}\n\t\t\tb[item.name] = pack\n\t\t\tfor i = 1, n do\n\t\t\t\tlocal s = item[i]\n\t\t\t\tlocal id = sprite_bank:add(s.cw, s.ch, s.x, s.y)\n\t\t\t\tsprite[id] = s\n\t\t\t\tpack[i] = id\n\t\t\tend\n\t\tend\n\tend\n\treturn b\nend\n\nlocal function load_from_file(filename)\n\tlocal b = bundle[filename]\n\tif b then\n\t\treturn b\n\tend\n\tlocal desc = spritebundle.load(filecache, filename)\n\tlocal b = add_list(desc)\n\tbundle[filename] = b\n\treturn b\nend\n\nlocal function load_from_table(t)\n\tlocal desc = spritebundle.load(filecache, t, t.path)\n\treturn add_list(desc)\nend\n\nfunction S.loadbundle(filename)\n\tif type(filename) == \"table\" then\n\t\treturn load_from_table(filename)\n\telse\n\t\treturn load_from_file(filename)\n\tend\nend\n\nfunction S.pack()\n\tlocal texid, n = sprite_bank:pack()\n\t-- upload rects into n textures, [texid, texid + n)\n\tlocal results = {}\n\tlocal texid_from = texid\n\tfor i = 1, n do\n\t\tlocal r = sprite_bank:altas(texid)\n\t\tfor id,v in pairs(r) do\n\t\t\tlocal x = v >> 32\n\t\t\tlocal y = v & 0xffffffff\n\t\t\tlocal obj = sprite[id]\n\t\t\tlocal c = filecache[obj.filename]\n\t\t\tlocal data = image.canvas(c.data, c.w, c.h, obj.cx, obj.cy, obj.cw, obj.ch)\n\t\t\tlocal w, h, ptr = image.canvas_size(data)\n\t\t\tr[id] = { id = id, data = ptr, x = x, y = y, w = w, h = h, stride = c.w * 4, dx = obj.x, dy = obj.y }\n\t\tend\n\t\ttexid = texid + 1\n\t\tresults[i] = r\n\tend\n\treturn results, texid_from\nend\n\nfunction S.write(id, filename)\n\tlocal obj = sprite[id]\n\tassert(obj.cx)\n\tlocal c = filecache[obj.filename]\n\tlocal data = image.canvas(c.data, c.w, c.h, obj.cx, obj.cy, obj.cw, obj.ch)\n\tlocal img = image.new(obj.cw, obj.ch)\n\timage.blit(img:canvas(), data)\n\timg:write(filename)\nend\n\nfunction S.preload(filename, content, w, h)\n\tassert(#content == w * h * 4)\n\tfilecache[filename] = { data = content, w = w, h = h }\nend\n\nreturn S"
  },
  {
    "path": "src/service/log.lua",
    "content": "local ltask = require \"ltask\"\nlocal writelog = require \"soluna.log\"\nlocal io = io\n\nglobal none\n\nlocal S = {}\n\nlocal sokol_log = writelog.sokol\nlocal ltask_log = writelog.ltask\n\nlocal function writelog()\n\tlocal flush\n\twhile true do\n\t\tlocal ti, id, msg, sz = ltask.poplog()\n\t\tif ti == nil then\n\t\t\tif flush then\n\t\t\t\tio.flush()\n\t\t\tend\n\t\t\tbreak\n\t\tend\n\t\tif id == 0 then\n\t\t\tsokol_log(ti, msg)\n\t\telse\n\t\t\tltask_log(ti, ltask.unpack_remove(msg, sz))\n\t\tend\n\t\tflush = true\n\tend\nend\n\nltask.fork(function()\n\twhile true do\n\t\twritelog()\n\t\tltask.sleep(20)\n\tend\nend)\n\nfunction S.quit()\n\twritelog()\nend\n\nreturn S\n"
  },
  {
    "path": "src/service/render.lua",
    "content": "local ltask = require \"ltask\"\nlocal render = require \"soluna.render\"\nlocal image = require \"soluna.image\"\nlocal embedsource = require \"soluna.embedsource\"\nlocal drawmgr = require \"soluna.drawmgr\"\nlocal file = require \"soluna.file\"\n\nglobal require, assert, pairs, pcall, ipairs, print, load, type\n\nlocal setting = require \"soluna\".settings()\n\nlocal font = {}\n\nlocal function create_materials(ctx)\n\tlocal materials = {}\n\n\tlocal function load_material(source, chunkname, id)\n\t\tlocal chunk = assert(load(source, chunkname))\n\t\tlocal mctx = {\n\t\t\tid = id,\n\t\t\tstate = ctx.state,\n\t\t\targ = ctx.arg,\n\t\t\ttmp_buffer = ctx.tmp_buffer,\n\t\t\tsettings = ctx.settings,\n\t\t\tfont = ctx.font,\n\t\t\trender = ctx.render,\n\t\t}\n\t\tlocal material = assert(chunk(mctx), chunkname .. \" : no material returned\")\n\t\tassert(type(material.submit) == \"function\", chunkname .. \" : missing submit function\")\n\t\tassert(type(material.draw) == \"function\", chunkname .. \" : missing draw function\")\n\t\tmaterials[id] = material\n\tend\n\n\tlocal MATERIAL_EXTLUA_BASE <const> = 256\n\tdo\n\t\tlocal next_id = 0\n\t\tfor _, name in ipairs(embedsource.material) do\n\t\t\tlocal id = next_id\n\t\t\tassert(id < MATERIAL_EXTLUA_BASE)\n\t\t\tnext_id = id + 1\n\t\t\tlocal loader = assert(embedsource.material[name])\n\t\t\tload_material(loader(), \"@src/material/\" .. name .. \".lua\", id)\n\t\tend\n\tend\n\tif setting.extlua_material then\n\t\tlocal list = setting.extlua_material\n\t\tif type(list) == \"string\" then\n\t\t\tlist = { list }\n\t\tend\n\t\tlocal path = assert(setting.extlua_material_path)\n\t\tlocal next_id = MATERIAL_EXTLUA_BASE\n\t\tfor _, name in ipairs(list) do\n\t\t\tlocal id = next_id\n\t\t\tnext_id = id + 1\n\t\t\tlocal fullname = assert(file.searchpath(name, path))\n\t\t\tload_material(file.load(fullname), \"@\" .. fullname, id)\n\t\tend\n\tend\n\treturn materials\nend\n\ndo\n\tlocal mgr = require \"soluna.font.manager\"\n\tlocal fontapi = require \"soluna.font\"\n\tlocal texture_ptr\n\n\tfunction font.init()\n\t\tmgr.init(embedsource.runtime.fontmgr(), \"@src/lualib/fontmgr.lua\")\n\t\tfont.texture_size = fontapi.texture_size\n\t\tfont.cobj = fontapi.cobj()\n\t\ttexture_ptr = fontapi.texture()\n\tend\n\n\tfunction font.shutdown()\n\t\tmgr.shutdown()\n\tend\n\n\tfunction font.submit(img)\n\t\tif fontapi.submit() then\n\t\t\timg:update(texture_ptr)\n\t\tend\n\tend\nend\n\nlocal batch = {}; do\n\tlocal thread\n\tlocal submit_n = 0\n\tfunction batch.register(addr)\n\t\tlocal n = #batch + 1\n\t\tbatch[n] = {\n\t\t\tsource = addr\n\t\t}\n\t\treturn n\n\tend\n\n\tfunction batch.wait()\n\t\tif submit_n ~= #batch then\n\t\t\tthread = ltask.current_token()\n\t\t\tltask.wait()\n\t\tend\n\t\tsubmit_n = 0\n\tend\n\n\tfunction batch.submit(id, ptr, size)\n\t\tlocal q = batch[id]\n\t\tlocal token = ltask.current_token()\n\t\tlocal function func()\n\t\t\treturn ptr, size, token\n\t\tend\n\t\tif q[1] == nil then\n\t\t\tsubmit_n = submit_n + 1\n\t\t\tif thread and submit_n == #batch then\n\t\t\t\tltask.wakeup(thread)\n\t\t\t\tthread = nil\n\t\t\tend\n\t\t\tq[1] = func\n\t\telse\n\t\t\tq[#q + 1] = func\n\t\tend\n\t\tltask.wait()\n\tend\n\n\tfunction batch.consume(id)\n\t\tlocal q = batch[id]\n\t\tlocal r = assert(q[1])\n\t\tlocal n = #q\n\t\tfor i = 1, n - 1 do\n\t\t\tq[i] = q[i + 1]\n\t\tend\n\t\tq[n] = nil\n\t\treturn r()\n\tend\nend\n\nlocal STATE\n\nlocal S = {}\n\nfunction S.app(settings)\n\tlocal soluna_app = require \"soluna.app\"\n\tfor k, v in pairs(settings) do\n\t\tlocal f = soluna_app[k]\n\t\tif f then\n\t\t\tf(v)\n\t\tend\n\tend\nend\n\n-- todo: update mutiple images\nlocal update_image\n\nlocal function delay_update_image(imgmem)\n\tfunction update_image()\n\t\tlocal from = imgmem.from\n\t\tfor i = 1, #imgmem do\n\t\t\tlocal tid = from + i\n\t\t\tlocal tex = STATE.textures[tid]\n\t\t\tif tex == nil then\n\t\t\t\tlocal texture_size = setting.texture_size\n\t\t\t\ttex = render.image {\n\t\t\t\t\twidth = texture_size,\n\t\t\t\t\theight = texture_size,\n\t\t\t\t}\n\t\t\t\tSTATE.textures[tid] = tex\n\t\t\t\tSTATE.views[tid] = render.view { texture = tex }\n\t\t\tend\n\t\t\ttex:update(imgmem[i])\n\t\tend\n\t\tupdate_image = nil\n\tend\nend\n\nlocal function frame(count)\n\tlocal batch_size = setting.batch_size\n\n\t-- todo: do not wait all batch commits\n\tlocal batch_n = #batch\n\tif update_image then update_image() end\n\tSTATE.drawmgr:reset()\n\tfor _, obj in pairs(STATE.materials) do\n\t\tif obj.reset then\n\t\t\tobj.reset()\n\t\tend\n\tend\n\n\tfor i = 1, batch_n do\n\t\tlocal ptr, size = batch[i][1]()\n\t\tif ptr then\n\t\t\tSTATE.drawmgr:append(ptr, size)\n\t\tend\n\tend\n\tlocal draw_n = #STATE.drawmgr\n\tfor i = 1, draw_n do\n\t\tlocal mat, ptr, n, tex = STATE.drawmgr(i)\n\t\tlocal obj = assert(STATE.materials[mat])\n\t\tobj.submit(ptr, n)\n\tend\n\tSTATE.srbuffer:update(STATE.srbuffer_mem:ptr())\n\tSTATE.pass:begin()\n\tfont.submit(STATE.font_texture)\n\tfor i = 1, draw_n do\n\t\tlocal mat, ptr, n, tex = STATE.drawmgr(i)\n\t\tlocal obj = assert(STATE.materials[mat])\n\t\tobj.draw(ptr, n, tex)\n\tend\n\tSTATE.pass:finish()\n\trender.submit()\nend\n\nfunction S.frame(count)\n\tbatch.wait()\n\tlocal ok, err = pcall(ltask.mainthread_run, frame, count)\n\tif not ok then\n\t\tprint(\"RENDER ERR\", err)\n\tend\n\tfor i = 1, #batch do\n\t\tlocal ptr, size, token = batch.consume(i)\n\t\tltask.wakeup(token)\n\tend\n\tassert(ok, err)\nend\n\nS.register_batch = assert(batch.register)\nS.submit_batch = assert(batch.submit)\n\nfunction S.quit()\n\tlocal workers = {}\n\tfor _, v in ipairs(batch) do\n\t\tworkers[v.source] = true\n\tend\n\n\tS.submit_batch = function() end -- prevent submit\n\n\tfor _, v in ipairs(batch) do\n\t\tfor _, resp in ipairs(v) do\n\t\t\tlocal _, _, token = resp()\n\t\t\tltask.wakeup(token)\n\t\tend\n\tend\n\n\tfor addr in pairs(workers) do\n\t\tltask.call(addr, \"quit\")\n\tend\n\tfont.shutdown()\nend\n\nfunction S.load_sprites(name)\n\tlocal loader = ltask.uniqueservice \"loader\"\n\tlocal spr = ltask.call(loader, \"loadbundle\", name)\n\tlocal rects, from = ltask.call(loader, \"pack\")\n\tlocal imgmems = { from = from }\n\tfor i = 1, #rects do\n\t\tlocal imgmem = image.new(setting.texture_size, setting.texture_size)\n\t\tlocal canvas = imgmem:canvas()\n\t\tfor id, v in pairs(rects[i]) do\n\t\t\tlocal src = image.canvas(v.data, v.w, v.h, v.stride)\n\t\t\timage.blit(canvas, src, v.x, v.y)\n\t\tend\n\t\timgmems[i] = imgmem\n\tend\n\tdelay_update_image(imgmems)\n\treturn spr\nend\n\nlocal function render_init(arg)\n\tfont.init()\n\n\tlocal texture_size = setting.texture_size\n\tlocal sr_buffer = render.buffer {\n\t\ttype = \"storage\",\n\t\tusage = \"dynamic\",\n\t\tlabel = \"texquad-scalerot\",\n\t\tsize = render.buffer_size(\"srbuffer\", setting.srbuffer_size),\n\t}\n\n\t-- todo: don't load texture here\n\n\tlocal font_texture = render.image {\n\t\twidth = font.texture_size,\n\t\theight = font.texture_size,\n\t\tpixel_format = \"R8\",\n\t}\n\tlocal views = {\n\t\tstorage = render.view { storage = sr_buffer },\n\t\tfont = render.view { texture = font_texture },\n\t}\n\n\tSTATE = {\n\t\tpass = render.pass {\n\t\t\tcolor0 = setting.background,\n\t\t\tswapchain = true,\n\t\t},\n\t\tdefault_sampler = render.sampler { label = \"texquad-sampler\" },\n\t\ttextures = {},\n\t\tfont_texture = font_texture,\n\t\tviews = views,\n\t}\n\tSTATE.srbuffer = assert(sr_buffer)\n\tSTATE.srbuffer_mem = render.srbuffer(setting.srbuffer_size)\n\n\tSTATE.drawmgr = drawmgr.new(arg.bank_ptr, setting.draw_instance)\n\n\tSTATE.uniform = render.uniform {\n\t\t12, -- size\n\t\tframesize = {\n\t\t\toffset = 0,\n\t\t\ttype = \"float\",\n\t\t\tn = 2,\n\t\t},\n\t\ttex_size = {\n\t\t\toffset = 8,\n\t\t\ttype = \"float\",\n\t\t},\n\t}\n\tSTATE.uniform.framesize = { 2 / arg.width, -2 / arg.height }\n\tSTATE.uniform.tex_size = 1 / texture_size\n\n\tlocal tmp_buffer = render.tmp_buffer(setting.tmpbuffer_size)\n\tSTATE.materials = create_materials {\n\t\tstate = STATE,\n\t\targ = arg,\n\t\ttmp_buffer = tmp_buffer,\n\t\tsettings = setting,\n\t\tfont = font,\n\t\trender = render,\n\t}\nend\n\nfunction S.init(arg)\n\tltask.mainthread_run(render_init, arg)\nend\n\nfunction S.resize(w, h)\n\tSTATE.uniform.framesize = { 2 / w, -2 / h }\nend\n\nreturn S\n"
  },
  {
    "path": "src/service/settings.lua",
    "content": "local initsetting = require \"soluna.initsetting\"\n\nglobal assert\n\nlocal S = {}\n\nlocal setting\n\nfunction S.init(args)\n\tassert(setting == nil)\n\tsetting = initsetting.init(args, true)\nend\n\nfunction S.get()\n\treturn setting\nend\n\nreturn S\n"
  },
  {
    "path": "src/service/start.lua",
    "content": "local ltask = require \"ltask\"\nlocal file = require \"soluna.file\"\nlocal spritemgr = require \"soluna.spritemgr\"\nlocal soluna = require \"soluna\"\nlocal soluna_app = require \"soluna.app\"\nlocal util = require \"soluna.util\"\nlocal table = table\nlocal debug = debug\n\nglobal error, tostring, assert, load, type, ipairs, pairs, xpcall, print, pcall\n\nlocal message_unpack = soluna_app.unpackmessage\n\nlocal args = ...\n\nlocal S = {}\n\nlocal app = {}\nlocal app_event = {}\nlocal prehook = {}\n\nfunction app.cleanup()\n\tltask.send(1, \"quit_ltask\")\nend\n\nlocal function skip()\n\tltask.mainthread_run(function() end)\nend\n\nlocal init_func\nfunction app.frame()\n\tif init_func then\n\t\tapp.frame = skip\n\t\tlocal ok , err_func = xpcall(init_func, debug.traceback)\n\t\tif ok then\n\t\t\tapp.frame = err_func or skip\n\t\telse\n\t\t\tskip()\n\t\t\tltask.log.error(err_func)\n\t\t\tsoluna_app.quit()\n\t\tend\n\tend\nend\n\nlocal render_service = ltask.self()\nlocal pre_size\n\nfunction prehook.window_resize(w, h)\n\tif render_service then\n\t\tltask.call(render_service, \"resize\", w, h)\n\telse\n\t\tpre_size = { width = w, height = h }\n\tend\nend\n\nS.event = skip\n\n-- external message from soluna host\nfunction S.external(p)\n\tlocal what, arg1, arg2 = message_unpack(p)\n\tlocal f = app[what]\n\tif f then\n\t\tf(arg1, arg2)\n\t\treturn\n\tend\n\tlocal pre = prehook[what]\n\tif pre then\n\t\tpre(arg1, arg2)\n\tend\n\tlocal f = app_event[what]\n\tif f then\n\t\tf(arg1, arg2)\n\tend\nend\n\nlocal cleanup = util.func_chain()\n\nlocal function init(arg)\n\tif arg == nil then\n\t\terror \"No command line args\"\n\tend\n\tsoluna.gamepad_init()\n\tlocal settings = ltask.uniqueservice \"settings\"\n\tltask.call(settings, \"init\", arg)\n\t\n\tlocal setting = soluna.settings()\n\tif setting.service_path then\n\t\tltask.servicepath(setting.service_path)\n\tend\n\t\n\tlocal audio = ltask.uniqueservice \"audio\"\n\tltask.call(audio, \"init_device\", arg.app.audio_device)\n\t\n\tlocal loader = ltask.uniqueservice \"loader\"\n\t\n\targ.app.bank_ptr = ltask.call(loader, \"init\", {\n\t\tmax_sprite = setting.sprite_max,\n\t\ttexture_size = setting.texture_size,\n\t})\n\t\n\tlocal entry = setting.entry\n\tlocal source = entry and file.load(entry)\n\tif not source then\n\t\terror (\"Can't load entry \" .. tostring(entry))\n\tend\n\tlocal f = assert(load(source, \"@\"..entry, \"t\"))\n\n\tlocal render = ltask.uniqueservice \"render\"\n\t\n\tlocal function init_render()\n\t\tltask.call(render, \"init\", arg.app)\n\t\trender_service = render\n\t\tif pre_size then\n\t\t\tltask.call(render, \"resize\", pre_size.width, pre_size.height)\n\t\t\targ.app.width = pre_size.width\n\t\t\targ.app.height = pre_size.height\n\t\t\tpre_size = nil\n\t\tend\n\t\t\n\t\tlocal batch = spritemgr.newbatch()\n\t\tcleanup:add(function()\n\t\t\tbatch:release()\n\t\tend)\n\t\t\n\t\tlocal callback = f {\n\t\t\tbatch = batch,\n\t\t\twidth = arg.app.width,\n\t\t\theight = arg.app.height,\n\t\t\ttable.unpack(arg),\n\t\t}\n\n\t\tif type(callback) ~= \"table\" then\n\t\t\tapp.frame = skip\n\t\t\tsoluna_app.close_window()\n\t\t\treturn\n\t\tend\n\t\t\n\t\tlocal frame_cb = callback.frame\n\t\n\t\tlocal messages = { \n\t\t\t\"mouse_move\", \"mouse_button\", \"mouse_scroll\", \"mouse\", \n\t\t\t\"touch_begin\", \"touch_end\", \"touch_moved\", \"touch_cancelled\",\n\t\t\t\"window_resize\", \"char\", \"key\",\n\t\t}\n\t\tlocal avail = {}\n\t\tfor _, v in ipairs(messages) do\n\t\t\tavail[v] = true\n\t\tend\n\t\tfor k,v in pairs(callback) do\n\t\t\tif avail[k] then\n\t\t\t\tapp_event[k] = v\n\t\t\tend\n\t\tend\n\t\t\n\t\tlocal batch_id = ltask.call(render, \"register_batch\", ltask.self())\n\n\t\tlocal function frame(count)\n\t\t\tbatch:reset()\n\t\t\tframe_cb(count)\n\t\t\tltask.send(render, \"submit_batch\", batch_id, batch:ptr())\n\t\t\tltask.call(render, \"frame\", count)\n\t\tend\n\t\t\n\t\tlocal traceback = debug.traceback\n\t\t\n\t\tlocal function render_frame(count)\n\t\t\tlocal ok, err = xpcall(frame, traceback, count)\n\t\t\tif not ok then\n\t\t\t\tapp.frame = skip\n\t\t\t\terror(err)\n\t\t\tend\n\t\tend\n\t\t\n\t\treturn render_frame\n\tend\n\n\treturn init_render()\nend\n\nfunction S.quit()\n\tcleanup()\nend\n\nltask.fork(function()\n\tltask.call(1, \"external_forward\", ltask.self(), \"external\")\n\n\t-- trigger INIT_EVENT, see main.lua\n\tskip()\n\n\tinit_func = function()\n\t\treturn init(args)\n\tend\nend)\n\n\nreturn S\n"
  },
  {
    "path": "src/sprite_submit.h",
    "content": "#ifndef soluna_sprite_submit_h\n#define soluna_sprite_submit_h\n\n#include \"batch.h\"\n#include <stdint.h>\n#include <assert.h>\n#include <math.h>\n\nstatic inline int32_t\nto_fixpoint_(float v) {\n\tv *= 256;\n\treturn (int)v;\n}\n\nstatic inline void\nsprite_set_xy(struct draw_primitive *p, float x, float y) {\n\tp->x = to_fixpoint_(x);\n\tp->y = to_fixpoint_(y);\n}\n\nstatic inline void\nsprite_apply_xy(struct draw_primitive *p, float x, float y) {\n\tp->x += to_fixpoint_(x);\n\tp->y += to_fixpoint_(y);\n}\n\nstatic inline int\nconvert_scale_(float scale) {\n\tassert(scale >= 0);\n\tif (scale >= 1.0f) {\n\t\t// scale is 20 bit fix point\n\t\tint fs = (int)((scale - 1.0f) * 256.0f);\n\t\t// max scale 1111, 1110, 1111,1111,1111\n\t\tconst int maxfs = 0xfefff;\n\t\tif (fs > maxfs)\n\t\t\tfs = maxfs;\n\t\treturn fs;\n\t}\n\t// use 12 bits for [0,1) scale\n\treturn 0xff000 | (int)(scale * 4096.0f);\n}\n\nstatic inline uint32_t\nconvert_scale_part_(uint32_t scale12) {\n\tuint32_t scale_fix;\n\tif (scale12 >= 0x1000) {\n\t\tscale_fix = (scale12 - 0x1000) >> 4;\n\t\tif (scale_fix > 0xfefff)\n\t\t\tscale_fix = 0xfefff;\n\t} else {\n\t\tscale_fix = scale12 | 0xff000;\n\t}\n\treturn scale_fix << 12;\n}\n\nstatic inline int\nconvert_rot_(float rot) {\n\tconst float pi = 3.1415927;\n\tfloat v = fmod(rot, 2 * pi);\n\tif (v < 0) {\n\t\tv += 2 * pi;\n\t}\n\treturn (int)(v * (2048.0f / pi));\n}\n\nstatic inline void\nsprite_set_scale(struct draw_primitive *p, float scale) {\n\tp->sr = convert_scale_(scale) << 12;\n}\n\nstatic inline void\nsprite_set_rot(struct draw_primitive *p, float rot) {\n\tp->sr = convert_rot_(rot);\n}\n\nstatic inline void\nsprite_set_sr(struct draw_primitive *p, float scale, float rot) {\n\tp->sr = convert_scale_(scale) << 12 | convert_rot_(rot);\n}\n\nstatic inline void\nsprite_apply_scale(struct draw_primitive *p, uint32_t scale_fix12) {\n\tuint32_t scale_fix = p->sr >> 12;\n\tif (scale_fix == 0) {\n\t\tscale_fix = convert_scale_part_(scale_fix12);\n\t} else {\n\t\tuint64_t s;\n\t\tif (scale_fix >= 0xff000) {\n\t\t\ts = scale_fix & 0xfff;\n\t\t} else {\n\t\t\ts = (scale_fix + 0x100) << 4;\n\t\t}\n\t\ts = (s * scale_fix12) >> 12;\n\t\tscale_fix = convert_scale_part_((uint32_t)s);\n\t}\n\tp->sr = scale_fix | (p->sr & 0xfff);\n}\n\n#endif"
  },
  {
    "path": "src/spritemgr.c",
    "content": "#include \"spritemgr.h\"\n#include \"sprite_submit.h\"\n#include \"batch.h\"\n#include \"transform.h\"\n\n#include <stdint.h>\n#include <string.h>\n#include <lua.h>\n#include <lauxlib.h>\n\n#define DEFAULT_TEXTURE_SIZE 4096\n#define INVALID_TEXTUREID 0xffff\n\n#define MAX_NODE 8192\n\n#define STB_RECT_PACK_IMPLEMENTATION\n#include \"stb/stb_rect_pack.h\"\n\nstatic int\nlbank_add(lua_State *L) {\n\tstruct sprite_bank *b = (struct sprite_bank *)luaL_checkudata(L, 1, \"SOLUNA_SPRITEBANK\");\n\tif (b->n >= b->cap) {\n\t\treturn luaL_error(L, \"Too many sprite (%d)\", b->n);\n\t}\n\tstruct sprite_rect *r = &b->rect[b->n++];\n\tint w = luaL_checkinteger(L, 2);\n\tint h = luaL_checkinteger(L, 3);\n\tint dx = luaL_optinteger(L, 4, 0);\n\tint dy = luaL_optinteger(L, 5, 0);\n\tif (w <= 0 || w > 0xffff || h <=0 || h >= 0xffff)\n\t\treturn luaL_error(L, \"Invalid sprite size (%d * %d)\", w, h);\n\tif (dx < -0x8000 || dx > 0x7fff || dy < -0x8000 || dy > 0x7ffff)\n\t\treturn luaL_error(L, \"Invalid sprite offset (%d * %d)\", dx, dy);\n\tr->u = w;\n\tr->v = h;\n\tr->off = (dx + 0x8000) << 16 | (dy + 0x8000);\n\tr->texid = INVALID_TEXTUREID;\n\tlua_pushinteger(L, b->n);\n\treturn 1;\n}\n\nstatic int\npack_sprite(struct sprite_bank *b, stbrp_context *ctx, stbrp_node *tmp, stbrp_rect *srect, int from, int reserved, int *reserved_n) {\n\tint last_texid = b->texture_n;\n\t\n\tstbrp_init_target(ctx, b->texture_size, b->texture_size, tmp, MAX_NODE);\n\tint i;\n\tint rect_i = reserved;\n\tfor (i=from;i<b->n;i++) {\n\t\tstruct sprite_rect *rect = &b->rect[i];\n\t\tif (rect->texid == INVALID_TEXTUREID || rect->texid == last_texid) {\n\t\t\trect->texid = last_texid;\n\t\t\tstbrp_rect * sr = &srect[rect_i++];\n\t\t\tsr->id = i;\n\t\t\t// reserve 1 pixel border\n\t\t\tsr->w = (rect->u + 1) & 0xffff;\n\t\t\tsr->h = (rect->v + 1) & 0xffff;\n\t\t\tif (sr->w > b->texture_size || sr->h > b->texture_size) {\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t}\n\tif (stbrp_pack_rects(ctx, srect, rect_i)) {\n\t\t// succ\n\t\tint j;\n\t\tfor (j=0;j<rect_i;j++) {\n\t\t\tstbrp_rect * sr = &srect[j];\n\t\t\tstruct sprite_rect *rect = &b->rect[sr->id];\n\t\t\trect->u = sr->x << 16 | (sr->w - 1);\n\t\t\trect->v = sr->y << 16 | (sr->h - 1);\n\t\t\trect->texid = last_texid;\n\t\t}\n\t\t*reserved_n = 0;\n\t} else {\n\t\t// pack a part\n\t\tint j;\n\t\tint n = 0;\n\t\tfor (j=0;j<rect_i;j++) {\n\t\t\tstbrp_rect * sr = &srect[j];\n\t\t\tstruct sprite_rect *rect = &b->rect[sr->id];\n\t\t\tif (sr->was_packed) {\n\t\t\t\trect->u = sr->x << 16 | (sr->w - 1);\n\t\t\t\trect->v = sr->y << 16 | (sr->h - 1);\n\t\t\t\trect->texid = last_texid;\n\t\t\t} else {\n\t\t\t\tstbrp_rect * tmp = &srect[n];\n\t\t\t\ttmp->w = sr->w;\n\t\t\t\ttmp->h = sr->h;\n\t\t\t\ttmp->id = sr->id;\n\t\t\t\t++n;\n\t\t\t}\n\t\t}\n\t\t*reserved_n = n;\n\t}\n\treturn i;\n}\n\nstruct tmp_context {\n\tstbrp_context ctx;\n\tstbrp_node tmp[MAX_NODE];\n\tstbrp_rect rect[1];// = malloc(sizeof(*rect) * b->n);\n};\n\nstatic inline struct tmp_context *\nalloc_context(int n) {\n\tstruct tmp_context * r = (struct tmp_context *)malloc(sizeof(*r) + (n - 1) * sizeof(r->rect));\n\treturn r;\n}\n\nstatic inline void\nfree_context(struct tmp_context *p) {\n\tfree(p);\n}\n\nstatic int\nlbank_pack(lua_State *L) {\n\tstruct sprite_bank *b = (struct sprite_bank *)luaL_checkudata(L, 1, \"SOLUNA_SPRITEBANK\");\n\tstruct tmp_context *ctx = alloc_context(b->n);\n\n\tint texture = b->texture_n;\n\tint from = 0;\n\tint reserved = 0;\n\tfor (;;) {\n\t\tfrom = pack_sprite(b, &ctx->ctx, ctx->tmp, ctx->rect, from, reserved, &reserved);\n\t\tif (from < 0) {\n\t\t\treturn luaL_error(L, \"sprite image is larger than texture\");\n\t\t}\n\t\tif (reserved == 0 && from >= b->n) {\n\t\t\tbreak;\n\t\t}\n\t\t++b->texture_n;\n\t}\n\tfree_context(ctx);\n\n\tlua_pushinteger(L, texture);\n\tlua_pushinteger(L, b->texture_n - texture + 1);\n\n\treturn 2;\n}\n\nstatic int\nlbank_altas(lua_State *L) {\n\tstruct sprite_bank *b = (struct sprite_bank *)luaL_checkudata(L, 1, \"SOLUNA_SPRITEBANK\");\n\tint tid = luaL_checkinteger(L, 2);\n\tint i;\n\tlua_newtable(L);\n\tfor (i=0;i<b->n;i++) {\n\t\tstruct sprite_rect *rect = &b->rect[i];\n\t\tif (rect->texid == tid) {\n\t\t\tuint64_t x = rect->u >> 16;\n\t\t\tuint64_t y = rect->v >> 16;\n\t\t\tuint64_t v = x << 32 | y;\n\t\t\tlua_pushinteger(L, v);\n\t\t\tlua_rawseti(L, -2, i + 1);\n\t\t}\n\t}\n\treturn 1;\n}\n\nstatic int\nlbank_ptr(lua_State *L) {\n\tstruct sprite_bank *b = (struct sprite_bank *)luaL_checkudata(L, 1, \"SOLUNA_SPRITEBANK\");\n\tlua_pushlightuserdata(L, b);\n\treturn 1;\n}\n\nstatic int\nlsprite_newbank(lua_State *L) {\n\tint cap = luaL_checkinteger(L, 1);\n\tint texture_size = luaL_optinteger(L, 2, DEFAULT_TEXTURE_SIZE);\n\tstruct sprite_bank *b = (struct sprite_bank *)lua_newuserdatauv(L, sizeof(*b) + (cap-1) * sizeof(b->rect[0]), 0);\n\tb->n = 0;\n\tb->cap = cap;\n\tb->texture_size = texture_size;\n\tb->texture_n = 0;\n\t\n\tif (luaL_newmetatable(L, \"SOLUNA_SPRITEBANK\")) {\n\t\tluaL_Reg l[] = {\n\t\t\t{ \"__index\", NULL },\n\t\t\t{ \"add\", lbank_add },\n\t\t\t{ \"pack\", lbank_pack },\n\t\t\t{ \"altas\", lbank_altas },\n\t\t\t{ \"ptr\", lbank_ptr },\n\t\t\t{ NULL, NULL },\n\t\t};\n\t\tluaL_setfuncs(L, l, 0);\n\n\t\tlua_pushvalue(L, -1);\n\t\tlua_setfield(L, -2, \"__index\");\n\t}\n\tlua_setmetatable(L, -2);\n\t\n\treturn 1;\n}\n\nstruct layer {\n\tfloat s;\n\tfloat r;\n\tfloat x;\n\tfloat y;\n};\n\nstruct batch {\n\tint n;\n\tint layer;\n\tint layer_cap;\n\tstruct transform trans;\n\tstruct draw_batch *b;\n\tstruct layer *stack;\n};\n\nstatic int\nlbatch_reset(lua_State *L) {\n\tstruct batch *b = (struct batch *)luaL_checkudata(L, 1, \"SOLUNA_BATCH\");\n\tb->n = 0;\n\tb->layer = 0;\n\tsprite_transform_identity(&b->trans);\n\treturn 0;\n}\n\nstatic int\nlbatch_release(lua_State *L) {\n\tstruct batch *b = (struct batch *)luaL_checkudata(L, 1, \"SOLUNA_BATCH\");\n\tb->n = 0;\n\tb->n = 0;\n\tbatch_delete(b->b);\n\tb->b = NULL;\n\treturn 0;\n}\n\nstatic struct draw_primitive *\nbatch_add_sprite(lua_State *L, struct batch *b) {\n\tint n = b->n;\n\tstruct draw_primitive * p = batch_reserve(b->b, n + 1);\n\tif (p == NULL)\n\t\tluaL_error(L, \"batch_add_sprite : Out of memory, n = %d\", n);\n\t\n\tp += n;\n\t\n\tint id = luaL_checkinteger(L, 2);\n\tif (id <= 0)\n\t\tluaL_error(L, \"Invalid sprite id %d\", id);\n\n\tp->x = 0;\n\tp->y = 0;\n\tp->sr = 0;\n\tp->sprite = id;\n\tb->n = n + 1;\n\n\treturn p;\n}\n\nstatic struct draw_primitive *\nbatch_add_material(lua_State *L, struct batch *b) {\n\tint n = b->n;\n\tstruct draw_primitive * p = batch_reserve(b->b, n + 2);\n\tif (p == NULL)\n\t\tluaL_error(L, \"batch_add_material : Out of memory, n = %d\", n);\n\n\tp += n;\n\t\n\tif (lua_getiuservalue(L, 2, 1) != LUA_TNUMBER)\n\t\tluaL_error(L, \"Invalid material object\");\n\tint matid = lua_tointeger(L, -1);\n\tif (matid <= 0)\n\t\tluaL_error(L, \"Invalid material id %d\", matid);\n\tlua_pop(L, 1);\n\n\tp->x = 0;\n\tp->y = 0;\n\tp->sr = 0;\n\tp->sprite = -matid;\n\n\tint sz = lua_rawlen(L, 2);\n\tif (sz > sizeof(struct draw_primitive))\n\t\tluaL_error(L, \"Invalid material object size (%d > %d)\", sz, sizeof(struct draw_primitive));\n\t\n\tmemcpy(p+1, lua_touserdata(L, 2), sz);\n\n\tb->n = n + 2;\n\n\treturn p;\n}\n\nstatic struct draw_primitive *\nbatch_add_stream(lua_State *L, struct batch *b, int *count) {\n\tint n = b->n;\n\tsize_t sz = 0;\n\tconst char * data = luaL_checklstring(L, 2, &sz);\n\tif (sz == 0)\n\t\treturn NULL;\n\t*count = sz / 2 / sizeof(struct draw_primitive);\n\tif (*count * 2 * sizeof(struct draw_primitive) != sz)\n\t\tluaL_error(L, \"Invalid stream size (%d)\", sz);\n\tstruct draw_primitive * p = batch_reserve(b->b, n + 2 * *count);\n\tif (p == NULL) {\n\t\tluaL_error(L, \"batch_add_stream : Out of memory n = %d count = %d\", n, *count);\n\t}\n\tp += n;\n\tb->n = n + *count * 2;\n\tmemcpy(p, data, *count * 2 * sizeof(struct draw_primitive));\n\treturn p;\n}\n\nstatic int\nlbatch_add(lua_State *L) {\n\tstruct batch *b = (struct batch *)luaL_checkudata(L, 1, \"SOLUNA_BATCH\");\n\tstruct draw_primitive *p;\n\tint n = 1;\n\tswitch (lua_type(L, 2)) {\n\tcase LUA_TNUMBER:\n\t\tp = batch_add_sprite(L, b);\n\t\tbreak;\n\tcase LUA_TUSERDATA:\n\t\tp = batch_add_material(L, b);\n\t\tbreak;\n\tcase LUA_TSTRING:\n\t\tp = batch_add_stream(L, b, &n);\n\t\tif (p == NULL)\n\t\t\treturn 0;\n\t\tbreak;\n\tdefault:\n\t\treturn luaL_error(L, \"Invalid type %s\", lua_typename(L, lua_type(L, 2)));\n\t}\n\tfloat x = luaL_optnumber(L, 3, 0);\n\tfloat y = luaL_optnumber(L, 4, 0);\n\t\n\tint i;\n\t\n\tfor (i=0;i<n;i++) {\n\t\tsprite_apply_xy(p, x, y);\n\t\tsprite_transform_apply(p, &b->trans);\n\t\tp+=2;\n\t}\n\treturn 0;\n}\n\nstatic int\nlbatch_ptr(lua_State *L) {\n\tstruct batch *b = (struct batch *)luaL_checkudata(L, 1, \"SOLUNA_BATCH\");\n\tint offset = luaL_optinteger(L, 2, 0);\n\tstruct draw_primitive * p = batch_reserve(b->b, 0);\n\tif (offset >= b->n)\n\t\treturn 0;\n\tp += offset;\n\tint n = b->n - offset;\n\tlua_pushlightuserdata(L, p);\n\tlua_pushinteger(L, n);\n\treturn 2;\n}\n\nstatic void\nlayer_close(lua_State *L, struct batch *b) {\n\tif (b->layer <= 0)\n\t\tluaL_error(L, \"none layer to close\");\n\tif (b->layer == 1) {\n\t\tb->layer = 0;\n\t\tsprite_transform_identity(&b->trans);\n\t} else {\n\t\t--b->layer;\n\t\tstruct layer *current = &b->stack[b->layer-1];\n\t\tsprite_transform_set(&b->trans, current->s, current->r, current->x, current->y);\n\t}\n}\n\nstatic struct layer *\nlayer_new(lua_State *L, struct batch *b) {\n\tif (b->layer >= b->layer_cap) {\n\t\tint cap = (b->layer + 1) * 3 / 2;\n\t\tstruct layer * tmp = (struct layer *)lua_newuserdatauv(L, cap * sizeof(struct layer), 0);\n\t\tmemcpy(tmp, b->stack, b->layer_cap * sizeof(struct layer));\n\t\tb->stack = tmp;\n\t\tb->layer_cap = cap;\n\t\tlua_setiuservalue(L, 1, 1);\n\t}\n\tstruct layer * ret = &b->stack[b->layer];\n\t++b->layer;\n\treturn ret;\n}\n\nstatic void \nlayer_merge(struct layer *current, struct layer *last) {\n\tfloat x, y;\n\tif (last->r != 0) {\n\t\tfloat sinv = sinf(last->r);\n\t\tfloat cosv = cosf(last->r);\n\t\ty = current->y * cosv + current->x * sinv;\n\t\tx = current->x * cosv - current->y * sinv;\n\t\tcurrent->r += last->r;\n\t} else {\n\t\tx = current->x;\n\t\ty = current->y;\n\t}\n\tif (last->s != 1) {\n\t\tx *= last->s;\n\t\ty *= last->s;\n\t\tcurrent->s *= last->s;\n\t}\n\tcurrent->x = x + last->x;\n\tcurrent->y = y + last->y;\n}\n\nstatic int\nlbatch_layer(lua_State *L) {\n\tstruct batch *b = (struct batch *)luaL_checkudata(L, 1, \"SOLUNA_BATCH\");\n\tstruct layer *new_layer = layer_new(L, b);\n\tswitch (lua_gettop(L)) {\n\tcase 1:\n\t\t// close layer\n\t\t--b->layer;\n\t\tlayer_close(L, b);\n\t\treturn 0;\n\tcase 2:\n\t\t// rot only\n\t\tnew_layer->s = 1;\n\t\tnew_layer->r = luaL_checknumber(L, 2);\n\t\tnew_layer->x = 0;\n\t\tnew_layer->y = 0;\n\t\tbreak;\n\tcase 3:\n\t\t// trans only\n\t\tnew_layer->s = 1;\n\t\tnew_layer->r = 0;\n\t\tnew_layer->x = luaL_checknumber(L, 2);\n\t\tnew_layer->y = luaL_checknumber(L, 3);\n\t\tbreak;\n\tcase 4:\n\t\t// st, no rot\n\t\tnew_layer->s = luaL_checknumber(L, 2);\n\t\tnew_layer->r = 0;\n\t\tnew_layer->x = luaL_checknumber(L, 3);\n\t\tnew_layer->y = luaL_checknumber(L, 4);\n\t\tbreak;\n\tcase 5:\n\t\t// srt\n\t\tnew_layer->s = luaL_checknumber(L, 2);\n\t\tnew_layer->r = luaL_checknumber(L, 3);\n\t\tnew_layer->x = luaL_checknumber(L, 4);\n\t\tnew_layer->y = luaL_checknumber(L, 5);\n\t\tbreak;\n\tdefault:\n\t\tluaL_error(L, \"Too many arguments\");\n\t}\n\tif (new_layer->s == 0)\n\t\tluaL_error(L, \"Scale can't be 0\");\n\tif (b->layer > 1) {\n\t\tlayer_merge(new_layer, new_layer-1);\n\t}\n\tsprite_transform_set(&b->trans, new_layer->s, new_layer->r, new_layer->x, new_layer->y);\n\treturn 0;\n}\n\nstatic int\nlbatch_point(lua_State *L) {\n\tstruct batch *b = (struct batch *)luaL_checkudata(L, 1, \"SOLUNA_BATCH\");\n\tif (b->layer == 0) {\n\t\tlua_settop(L, 3);\n\t\treturn 2;\n\t}\n\tfloat x = luaL_checknumber(L, 2);\n\tfloat y = luaL_checknumber(L, 3);\n\t\n\t// to .8 fix number\n\tint fx = (int)(x * 256);\n\tint fy = (int)(y * 256);\n\t\n\tsprite_transform_point(&b->trans, &fx, &fy);\n\t\n\tlua_pushnumber(L, (float)fx / 256.0f);\n\tlua_pushnumber(L, (float)fy / 256.0f);\n\treturn 2;\n}\n\nstatic int\nlsprite_newbatch(lua_State *L) {\n\tstruct batch *b = (struct batch *)lua_newuserdatauv(L, sizeof(*b), 1);\n\tb->n = 0;\n\tb->b = batch_new(0);\n\tif (b->b == NULL)\n\t\treturn luaL_error(L, \"sprite_newbatch : Out of memory\");\n\tb->layer = 0;\n\tb->layer_cap = 0;\n\tb->stack = NULL;\n\tsprite_transform_identity(&b->trans);\n\t\t\n\tif (luaL_newmetatable(L, \"SOLUNA_BATCH\")) {\n\t\tluaL_Reg l[] = {\n\t\t\t{ \"__index\", NULL },\n\t\t\t{ \"reset\", lbatch_reset },\n\t\t\t{ \"add\", lbatch_add },\n\t\t\t{ \"ptr\", lbatch_ptr },\n\t\t\t{ \"release\", lbatch_release },\n\t\t\t{ \"layer\", lbatch_layer },\n\t\t\t{ \"point\", lbatch_point },\n\t\t\t{ NULL, NULL },\n\t\t};\n\t\tluaL_setfuncs(L, l, 0);\n\n\t\tlua_pushvalue(L, -1);\n\t\tlua_setfield(L, -2, \"__index\");\n\t}\n\tlua_setmetatable(L, -2);\n\t\n\treturn 1;\n}\n\nint\nluaopen_spritemgr(lua_State *L) {\n\tluaL_checkversion(L);\n\tluaL_Reg l[] = {\n\t\t{ \"newbank\", lsprite_newbank },\n\t\t{ \"newbatch\", lsprite_newbatch },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\tsprite_transform_init();\n\treturn 1;\n}\n"
  },
  {
    "path": "src/spritemgr.h",
    "content": "#ifndef soluna_spritemgr_h\n#define soluna_spritemgr_h\n\n#include <stdint.h>\n#include <assert.h>\n\n#define INVALID_TEXTUREID 0xffff\n\nstruct sprite_rect {\n\tuint32_t texid;\n\tuint32_t off;\t// (dx + 0x8000) << (dy + 0x8000)\n\tuint32_t u;\t\t// x << 16 | w\n\tuint32_t v;\t\t// y << 16 | h\n};\n\nstruct sprite_bank {\n\tint n;\n\tint cap;\n\tint texture_size;\n\tint texture_n;\n\tstruct sprite_rect rect[1];\n};\n\n#endif\n"
  },
  {
    "path": "src/srbuffer.c",
    "content": "#include \"srbuffer.h\"\n#include <math.h>\n#include <stdlib.h>\n\nstatic inline int\npow2(int n) {\n\tint m = 1 << 5;\n\twhile (m < n) {\n\t\tm *= 2;\n\t}\n\treturn m;\n}\n\nsize_t\nsrbuffer_size(int n) {\n\tstruct sr_buffer *dummy = NULL;\n\tn = pow2(n);\n\tsize_t sz = sizeof(*dummy->frame) +\n\t\tsizeof(*dummy->cache) +\n\t\tsizeof(*dummy->key) +\n\t\tsizeof(*dummy->data);\n\treturn sizeof(*dummy) + sz * n;\n}\n\nvoid\nsrbuffer_init(struct sr_buffer *SR, int n) {\n\tn = pow2(n);\n\tuint8_t *ptr = (uint8_t *)(SR + 1);\n\tSR->data = (struct sr_mat *)ptr;\n\tptr += n * sizeof(SR->data[0]);\n\tSR->key = (uint32_t *)ptr;\n\tptr += n * sizeof(SR->key[0]);\n\tSR->cache = (uint16_t *)ptr;\n\tptr += n * sizeof(SR->cache[0]);\n\tSR->frame = ptr;\n\n\tSR->cap = n;\n\tSR->n = 1;\n\tSR->dirty = 1;\n\tSR->current_frame = 0;\n\tSR->current_n = 1;\n\tSR->frame[0] = 0;\n\tSR->key[0] = 0;\n\tSR->cache[0] = 0;\n\tfloat *v = SR->data[0].v;\n\tv[0] = 1.0f; v[1] = 0;\n\tv[2] = 0; v[3] = 1.0f;\n}\n\nint\nsrbuffer_add(struct sr_buffer *SR, uint32_t v) {\n\tint index = v % (SR->cap - 1);\n\tint slot = SR->cache[index];\n\tif (slot < SR->n && v == SR->key[slot]) {\n\t\tif (SR->frame[slot] != SR->current_frame) {\n\t\t\tSR->frame[slot] = SR->current_frame;\n\t\t\t++SR->current_n;\n\t\t}\n\t\treturn slot;\n\t}\n\tif (SR->n >= SR->cap) {\n\t\tint i;\n\t\tfor (i=1;i<SR->cap;i++) {\n\t\t\tif (SR->key[i] == v) {\n\t\t\t\tSR->cache[index] = i;\n\t\t\t\tSR->frame[index] = SR->current_frame;\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t\treturn -1;\t// full\n\t}\n\tint new_slot = 1;\n\tif (SR->current_n * 2 < SR->n) {\n\t\t// find an exist slot\n\t\tslot = v % SR->n;\n\t\tint i;\n\t\tfor (i=0;i<SR->n;i++) {\n\t\t\tif (SR->frame[slot] != SR->current_frame) {\n\t\t\t\tSR->frame[slot] = SR->current_frame;\n\t\t\t\t++SR->current_n;\n\t\t\t\tnew_slot = 0;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t++slot;\n\t\t\tif (slot >= SR->n)\n\t\t\t\tslot -= SR->n;\n\t\t}\n\t}\n\tif (new_slot) {\n\t\tslot = SR->n++;\n\t\tSR->frame[slot] = SR->current_frame;\n\t\t++SR->current_n;\n\t}\n\tSR->dirty = 1;\n\tSR->cache[index] = slot;\n\tSR->key[slot] = v;\n\tfloat *mat = SR->data[slot].v;\n\tuint32_t scale_fix = v >> 12;\n\tfloat scale = 1.0f;\n\tif (scale_fix != 0) {\n\t\tif (scale_fix >= 0xff000) {\n\t\t\tscale = (float)(scale_fix & 0xfff) * (1.0f / 4096.0f);\n\t\t} else {\n\t\t\tscale = (float)scale_fix * (1.0f / 256.0f) + 1.0f;\n\t\t}\n\t}\n\tuint32_t rot_fix = v & 0xfff;\n\tif (rot_fix == 0) {\n\t\tmat[0] = scale; mat[1] = 0;\n\t\tmat[2] = 0; mat[3] = scale;\n\t} else {\n\t\tconst float pi = 3.1415927f;\n\t\tfloat rot = (float) rot_fix * ( pi / 2048.0f );\n\t\tfloat cosr = cosf(rot) * scale;\n\t\tfloat sinr = sinf(rot) * scale;\n\t\tmat[0] = cosr; mat[1] = -sinr;\n\t\tmat[2] = sinr; mat[3] = cosr;\n\t}\n\treturn slot;\n}\n\nvoid *\nsrbuffer_commit(struct sr_buffer *SR, int *sz) {\n\tif (SR->dirty) {\n\t\t*sz = SR->n * sizeof(SR->data[0]);\n\t\tSR->dirty = 0;\n\t\tSR->current_n = 1;\n\t\t++SR->current_frame;\n\t\tSR->frame[0] = SR->current_frame;\n\t\treturn SR->data;\n\t}\n\t*sz = 0;\n\treturn NULL;\n}\n\n#ifdef TEST_SRBUFFER_MAIN\n\n#include <stdio.h>\n#include <assert.h>\n#include \"sprite_submit.h\"\n\nstatic void\ntest(float x, float y, float scale, float rad, uint32_t *sr, float *ox, float *oy) {\n\tstruct draw_primitive tmp;\n\tconst float pi = 3.1415927f;\n\tsprite_set_sr(&tmp, scale, rad * pi / 180.0f);\n\tunion {\n\t\tstruct sr_buffer buffer;\n\t\tuint8_t tmp[65535];\n\t} u;\n\tassert(srbuffer_size(1024) < sizeof(u.tmp));\n\tsrbuffer_init(&u.buffer, 1024);\n\tint index = srbuffer_add(&u.buffer, tmp.sr);\n\tconst float *mat = u.buffer.data[index].v;\n\t*ox = x * mat[0] + y * mat[1];\n\t*oy = x * mat[2] + y * mat[3];\n\t*sr = tmp.sr;\n}\n\nstatic void\ntest_rot(float x, float y, float rot) {\n\tfloat ox,oy;\n\tuint32_t v;\n\ttest(x, y, 1, rot, &v, &ox, &oy);\n\tprintf(\"[%f,%f / %f] =(%x)=> [%f,%f]\\n\", x, y, rot, v, ox, oy);\n}\n\nstatic void\ntest_sr(float x, float y, float scale, float rot) {\n\tfloat ox,oy;\n\tuint32_t v;\n\ttest(x, y, scale, rot, &v, &ox, &oy);\n\tprintf(\"[%f,%f / %f,%f] =(%x)=> [%f,%f]\\n\", x, y, scale, rot, v, ox, oy);\n}\n\nint\nmain() {\n\ttest_rot(100, 100, 45);\n\ttest_rot(100, 0, 90);\n\ttest_sr(100, 0, 1.5, 30);\n\ttest_sr(100, 0, 0.5, -60);\n\treturn 0;\n}\n\n#endif\n"
  },
  {
    "path": "src/srbuffer.h",
    "content": "#ifndef soluna_srbuffer_h\n#define soluna_srbuffer_h\n\n#include <stdint.h>\n#include <stdlib.h>\n\nstruct sr_mat {\n\tfloat v[4];\n};\n\nstruct sr_buffer {\n\tint n;\n\tint cap;\n\tint current_n;\n\tuint8_t dirty;\n\tuint8_t current_frame;\n\tuint8_t *frame;\n\tuint16_t *cache;\n\tuint32_t *key;\n\tstruct sr_mat *data;\n};\n\nsize_t srbuffer_size(int n);\nvoid srbuffer_init(struct sr_buffer *SR, int n);\nint srbuffer_add(struct sr_buffer *SR, uint32_t sr);\nvoid * srbuffer_commit(struct sr_buffer *SR, int *sz);\n\n#endif\n"
  },
  {
    "path": "src/texquad.glsl",
    "content": "@vs vs\nlayout(binding=0) uniform vs_params {\n\tvec2 framesize;\n\tfloat texsize;\n};\n\nstruct sr_mat {\n\tmat2 m;\n};\n\nlayout(binding=0) readonly buffer sr_lut {\n\tsr_mat sr[];\n};\n\nin vec3 position;\nin uint offset;\nin uint u;\nin uint v;\n\nout vec2 uv;\n\nvoid main() {\n\tivec2 uv_base = ivec2(u >> 16, v >> 16);\n\tivec2 u2 = ivec2(0 , u & 0xffff);\n\tivec2 v2 = ivec2(0 , v & 0xffff);\n\tivec2 off = ivec2(offset >> 16 , offset & 0xffff) - 0x8000;\n\tvec2 uv_offset = vec2(u2[gl_VertexIndex & 1] , v2[gl_VertexIndex >> 1]);\n\tvec2 pos = ((uv_offset - off) * sr[int(position.z)].m + position.xy) * framesize;\n\tgl_Position = vec4(pos.x - 1.0f, pos.y + 1.0f, 0, 1);\n\tuv = (uv_base + uv_offset) * texsize;\n}\n\n@end\n\n@fs fs\nlayout(binding=1) uniform texture2D tex;\nlayout(binding=0) uniform sampler smp;\n\nin vec2 uv;\nout vec4 frag_color;\n\nvoid main() {\n\tfrag_color = texture(sampler2D(tex,smp), uv);\n}\n@end\n\n@program texquad vs fs"
  },
  {
    "path": "src/tmpbuffer.h",
    "content": "#ifndef soluna_tmp_buffer_h\n#define soluna_tmp_buffer_h\n\n#include <lua.h>\n#include <lauxlib.h>\n\nstruct tmp_buffer {\n\tvoid *ptr;\n\tsize_t sz;\n};\n\n#define TMPBUFFER_PTR(type, obj) (type *)((obj)->ptr)\n#define TMPBUFFER_SIZE(type, obj) ((obj)->sz / sizeof(type))\n\nstatic inline void\ntmp_buffer_init(lua_State *L, struct tmp_buffer *tmp, int uv_index, const char *key) {\n\tif (lua_getfield(L, 1, key) != LUA_TUSERDATA)\n\t\tluaL_error(L, \"Invalid key .%s\", key);\n\tif (lua_type(L, -1) != LUA_TUSERDATA || lua_getmetatable(L, -1)) {\n\t\tluaL_error(L, \"Not an userdata without metatable\");\n\t}\n\ttmp->ptr = lua_touserdata(L, -1);\n\ttmp->sz = lua_rawlen(L, -1);\n\t// ud, object\n\tlua_setiuservalue(L, -2, uv_index);\n}\n\n#endif\n"
  },
  {
    "path": "src/transform.c",
    "content": "#include <math.h>\n#include \"batch.h\"\n#include \"transform.h\"\n#include \"sprite_submit.h\"\n\n// sin(0 ~ 2 * pi) * 2^24\nstatic int sin_lut[4096];\n\nvoid\nsprite_transform_init() {\n\tstatic int init = 0;\n\t// Don't card about race condition, because sin_lut is a constant table\n\tif (init)\n\t\treturn;\n\tint i;\n\tconst float pi = 3.1415927f;\n\tconst float pow2 = (float)(1<<24);\n\tfor (i=0;i<4096;i++) {\n\t\tsin_lut[i] = (int)(sinf((float)i / 2048.0f * pi) * pow2);\n\t}\n\tinit = 1;\n}\n\nstatic inline void\nsincos_lut(int d, int *sin, int *cos) {\n\tint x = (unsigned)d;\n\t*sin = sin_lut[x];\n\t*cos = sin_lut[(4096 + 1024 - x) % 4096];\n}\n\nvoid\nsprite_transform_apply(struct draw_primitive *p, struct transform * t) {\n\tint64_t x, y;\n\t\n\tif (t->r != 0) {\n\t\tint sin, cos;\n\t\tsincos_lut(t->r, &sin, &cos);\n\n\t\tint64_t x0 = p->x;\n\t\tint64_t y0 = p->y;\n\t\ty = y0 * cos + x0 * sin;\n\t\tx = x0 * cos - y0 * sin;\n\t\t\n\t\tx >>= 24;\n\t\ty >>= 24;\n\n\t\tint r = p->sr & 0xfff;\n\t\tr = (r + t->r) % 4096;\n\t\tp->sr = (p->sr & ~0xfff) | r;\n\t} else {\n\t\tx = p->x;\n\t\ty = p->y;\n\t}\n\n\tif (t->s != 0x1000) {\n\t\tx *= t->s;\t// .(12 + 12) fix number\n\t\ty *= t->s;\n\t\tx >>= 12;\n\t\ty >>= 12;\n\t\tsprite_apply_scale(p, t->s);\n\t}\n\tp->x = (int32_t)x + t->x;\n\tp->y = (int32_t)y + t->y;\n}\n\nvoid\nsprite_transform_set(struct transform *t, float s, float r, float x, float y) {\n\tt->s = (int32_t)(s * 4096);\n\tconst float rot_scale = 2048.0 / 3.1415927;\n\tt->r = (int)(r * rot_scale) % 4096;\n\tt->x = (int32_t)(x * 256);\n\tt->y = (int32_t)(y * 256);\n}\n\n// returns origin point (x, y) in transformed coordinate system\nvoid\nsprite_transform_point(const struct transform *t, int *x, int *y) {\n\tint64_t ox = *x;\n\tint64_t oy = *y;\n\n\tox -= t->x;\n\toy -= t->y;\n\tif (t->r != 0) {\n\t\tint sin, cos;\n\t\tsincos_lut(t->r, &sin, &cos);\n\n\t\tint64_t x = ox;\n\t\tint64_t y = oy;\n\t\tox = x * cos + y * sin;\n\t\toy = y * cos - x * sin;\n\t\tox >>= 24;\n\t\toy >>= 24;\n\t}\n\tif (t->s != 4096) {\n\t\tint s = t->s;\n\t\tif (s == 0)\n\t\t\ts = 1;\n\t\tint32_t inv_s = (1 << 30) / s;\t// .18bits fix\n\t\tox *= inv_s;\n\t\toy *= inv_s;\n\t\t\n\t\tox >>= 18;\n\t\toy >>= 18;\n\t}\n\t*x = (int)ox;\n\t*y = (int)oy;\n}\n"
  },
  {
    "path": "src/transform.h",
    "content": "#ifndef soluna_transform_h\n#define soluna_transform_h\n\n#include <stdint.h>\n\nstruct transform {\n\t// ix, iy : sign bit + 23.8   fix number\n\tint32_t x;\t\n\tint32_t y;\n\tint32_t s; // sign bit + 19.12 fix number\n\tint r;\t// 12bits [0,4095]\n};\n\nstruct draw_primitive;\n\nstatic inline void\nsprite_transform_identity(struct transform * t) {\n\tt->x = 0;\n\tt->y = 0;\n\tt->s = 1 << 12;\n\tt->r = 0;\n}\n\nvoid sprite_transform_init();\nvoid sprite_transform_set(struct transform *t, float s, float r, float x, float y);\nvoid sprite_transform_apply(struct draw_primitive *p, struct transform * t);\nvoid sprite_transform_point(const struct transform *t, int *x, int *y);\n\n#endif\n"
  },
  {
    "path": "src/truetype.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n#include <string.h>\n\n#include \"font_define.h\"\n#include \"truetype.h\"\n\nstatic const unsigned char *\nget_ttfbuffer(lua_State *L, int index) {\n\tint t = lua_type(L, index);\n\tif (t == LUA_TSTRING) {\n\t\treturn (const unsigned char *)lua_tostring(L, index);\n\t} else if (t == LUA_TUSERDATA || t == LUA_TLIGHTUSERDATA) {\n\t\treturn (const unsigned char *)lua_touserdata(L, index);\n\t}\n\tluaL_error(L, \"Invalid ttfbuffer type = %s\", lua_typename(L, t));\n\treturn NULL;\n}\n\n// integer fontid (base 1)\n// string/userdata fontdata\n// integer index / string name\nstatic int\nlupdate_cstruct(lua_State *L) {\n\tstruct truetype_font * f = truetype_cstruct(L);\n\tint fontid = luaL_checkinteger(L, 1);\n\tif (fontid < 1 || fontid > MAX_FONT_NUM)\n\t\treturn luaL_error(L, \"The font id %d is out of %d\", fontid, MAX_FONT_NUM);\n\tconst unsigned char * data = get_ttfbuffer(L, 2);\n\tif (data == NULL)\n\t\treturn luaL_error(L, \"Invalid font data for %d\", fontid);\n\tint type = lua_type(L, 3);\n\tint offset = 0;\n\tif (type == LUA_TSTRING) {\n\t\toffset = stbtt_FindMatchingFont(data, lua_tostring(L, 3), STBTT_MACSTYLE_DONTCARE);\n\t\tif (offset < 0)\n\t\t\treturn luaL_error(L, \"Can't find %s in font %d\",  lua_tostring(L, 3), fontid);\n\t} else {\n\t\tint index = luaL_optinteger(L, 3, 0);\n\t\toffset = stbtt_GetFontOffsetForIndex(data, index);\n\t\tif (offset < 0)\n\t\t\treturn luaL_error(L, \"Invalid offset for font %d index %d\", fontid, index);\n\t}\n\n\t--fontid;\n\tif (stbtt_InitFont(&f->fontinfo[fontid], data, offset) == 0)\n\t\treturn luaL_error(L, \"InitFont %d with failed\", fontid+1);\n\tf->enable |= (uint64_t)(1 << fontid);\n\n\tlua_pushlightuserdata(L, &f->fontinfo[fontid]);\n\treturn 1;\n}\n\n// integer fontid (base 1)\nstatic int\nlunload_cstruct(lua_State *L) {\n\tstruct truetype_font * f = truetype_cstruct(L);\n\tint fontid = luaL_checkinteger(L, 1);\n\tif (fontid < 1 || fontid > MAX_FONT_NUM)\n\t\treturn luaL_error(L, \"The font id %d is out of %d\", fontid, MAX_FONT_NUM);\n\t--fontid;\n\tf->enable &= ~(1 << fontid);\n\treturn 0;\n}\n\n// string/userdata data\n// integer index\n// integer platid\n// integer encodeid\n// integer langid\n// return string utf-16 family, sub-family\nstatic int\nlnamestring(lua_State *L) {\n\tconst unsigned char * data = get_ttfbuffer(L, 1);\n\tint index = luaL_checkinteger(L, 2);\n\tstbtt_fontinfo font;\n\tint offset = stbtt_GetFontOffsetForIndex(data, index);\n\tif (offset < 0) {\n\t\treturn 0;\n\t}\n\tif (stbtt_InitFont(&font, data, offset) == 0)\n\t\treturn luaL_error(L, \"InitFont with index %d failed\", index);\n\tint platid = luaL_checkinteger(L, 3);\n\tint encodeid = luaL_checkinteger(L, 4);\n\tint langid = luaL_checkinteger(L, 5);\n\tint len = 0;\n\tconst char * family = stbtt_GetFontNameString(&font, &len, platid, encodeid, langid, 1);\n\tif (family) {\n\t\tlua_pushlstring(L, family, len);\n\t} else {\n\t\tlua_pushboolean(L, 0);\n\t\treturn 1;\n\t}\n\tconst char * subfam = stbtt_GetFontNameString(&font, &len, platid, encodeid, langid, 2);\n\tif (subfam) {\n\t\tlua_pushlstring(L, subfam, len);\n\t} else {\n\t\treturn 1;\n\t}\n\treturn 2;\n}\n\nstatic void\ninit_cstruct(lua_State *L) {\n\tstruct truetype_font *f = (struct truetype_font *)lua_newuserdatauv(L, sizeof(*f), 0);\n\tf->enable = 0;\n\tlua_setfield(L, LUA_REGISTRYINDEX, TRUETYPE_CSTRUCT);\n}\n\nstatic int\nltestname(lua_State *L) {\n\tconst char * name = luaL_checkstring(L, 1);\n\tint id = truetype_name(L, name);\n\tlua_pushinteger(L, id);\n\treturn 1;\n}\n\nstatic int\nltestinfo(lua_State *L) {\n\tint id = luaL_checkinteger(L, 1);\n\tstruct truetype_font * f = truetype_cstruct(L);\n\tconst stbtt_fontinfo *info = truetype_font(f, id, L);\n\tlua_pushlightuserdata(L, (void *)info);\n\treturn 1;\n}\n\nint\nluaopen_font_truetype(lua_State *L) {\n\tluaL_checkversion(L);\n\tinit_cstruct(L);\n\tluaL_Reg l[] = {\n\t\t{ \"update\", lupdate_cstruct },\n\t\t{ \"unload\", lunload_cstruct },\n\t\t{ \"namestring\", lnamestring },\n\t\t{ \"testname\", ltestname },\t// test C api : truetype_name\n\t\t{ \"testinfo\", ltestinfo },\t// test C api : truetype_font\n\t\t{ \"nametable\", NULL },\n\t\t{ \"idtable\", NULL },\n\t\t{ \"enum\", NULL },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\n\tlua_newtable(L);\n\tlua_pushvalue(L, -1);\n\tlua_setfield(L, LUA_REGISTRYINDEX, TRUETYPE_NAME);\n\tlua_setfield(L, -2, \"nametable\");\n\n\tlua_newtable(L);\n\tlua_pushvalue(L, -1);\n\tlua_setfield(L, LUA_REGISTRYINDEX, TRUETYPE_ID);\n\tlua_setfield(L, -2, \"idtable\");\n\n\tlua_newtable(L);\n\tlua_pushvalue(L, -1);\n\tlua_setfield(L, LUA_REGISTRYINDEX, TRUETYPE_ENUM);\n\tlua_setfield(L, -2, \"enum\");\n\n\treturn 1;\n}\n"
  },
  {
    "path": "src/truetype.h",
    "content": "#ifndef ant_truetype_h\n#define ant_truetype_h\n\n#include <lua.h>\n#include <lauxlib.h>\n#include <stdint.h>\n#include <string.h>\n\n#include <stb/stb_truetype.h>\n\n#define TRUETYPE_ID \"TRUETYPE_ID\"\n#define TRUETYPE_NAME \"TRUETYPE_NAME\"\n#define TRUETYPE_ENUM \"TRUETYPE_ENUM\"\n#define TRUETYPE_CSTRUCT \"TRUETYPE_CSTRUCT\"\n#define TRUETYPE_IMPORT \"TRUETYPE_IMPORT\"\n\nstruct truetype_font {\n\tuint64_t enable;\n\tstbtt_fontinfo fontinfo[MAX_FONT_NUM];\n};\n\n// get global struct truetype_font\nstatic inline struct truetype_font *\ntruetype_cstruct(lua_State *L) {\n\tif (lua_getfield(L, LUA_REGISTRYINDEX, TRUETYPE_CSTRUCT) != LUA_TUSERDATA) {\n\t\tlua_pop(L, 1);\n\t\treturn NULL;\n\t}\n\tstruct truetype_font *ret = (struct truetype_font *)lua_touserdata(L, -1);\n\tlua_pop(L, 1);\n\treturn ret;\n}\n\nstatic inline int\nlget_fontdata(lua_State *L) {\n\tint fontid = (int)lua_tointeger(L, 1);\n\tif (lua_getfield(L, LUA_REGISTRYINDEX, TRUETYPE_ID) != LUA_TTABLE) {\n\t\treturn 0;\n\t}\n\tlua_geti(L, -1, fontid);\n\treturn 1;\n}\n\nstatic inline const stbtt_fontinfo *\ndefault_info(struct truetype_font *ttf) {\n\tif (ttf->enable & 1)\n\t\treturn &ttf->fontinfo[0];\n\treturn NULL;\n}\n\n// font id -> font info\nstatic inline const stbtt_fontinfo *\ntruetype_font(struct truetype_font *ttf, int fontid, lua_State *L) {\n\tif (fontid < 1 || fontid > MAX_FONT_NUM) {\n\t\tfontid = 0;\n\t} else {\n\t\t--fontid;\n\t}\n\n\tif (ttf->enable & (uint64_t)1 << fontid) {\n\t\treturn &ttf->fontinfo[fontid];\n\t}\n\tif (L == NULL) {\n\t\treturn default_info(ttf);\n\t}\n\n\tlua_pushcfunction(L, lget_fontdata);\n\tlua_pushinteger(L, fontid+1);\n\tif (lua_pcall(L, 1, 1, 0) != LUA_OK) {\n\t\tprintf(\"TRUETYPE_ID err: %s\\n\", lua_tostring(L, -1));\n\t\tlua_pop(L, 1);\n\t\treturn default_info(ttf);\n\t}\n\n\tconst stbtt_fontinfo * info = (const stbtt_fontinfo*)lua_touserdata(L, -1);\n\tlua_pop(L, 1);\n\tif (info == NULL) {\n\t\treturn default_info(ttf);\n\t}\n\treturn info;\n}\n\nstatic inline int\nlget_fontid(lua_State *L) {\n\tconst char *name = (const char *)lua_touserdata(L, 1);\n\tif (lua_getfield(L, LUA_REGISTRYINDEX, TRUETYPE_NAME) != LUA_TTABLE) {\n\t\treturn 0;\n\t}\n\tlua_getfield(L, -1, name);\n\treturn 1;\n}\n\n// font name -> font id\nstatic inline int\ntruetype_name(lua_State *L, const char *name) {\n\tlua_pushcfunction(L, lget_fontid);\n\tlua_pushlightuserdata(L, (void *)name);\n\tif (lua_pcall(L, 1, 1, 0) != LUA_OK) {\n\t\tprintf(\"TRUETYPE_NAME err: %s\\n\", lua_tostring(L, -1));\n\t\tlua_pop(L, 1);\n\t\treturn 0;\n\t}\n\tint fontid = 0;\n\tif (lua_type(L, -1) == LUA_TNUMBER) {\n\t\tfontid = (int)lua_tointeger(L, -1);\n\t}\n\tlua_pop(L, 1);\n\treturn fontid;\n}\n\nstatic inline int\nlenum_fontname(lua_State *L) {\n\tint idx = lua_tointeger(L, 1);\n\tif (lua_getfield(L, LUA_REGISTRYINDEX, TRUETYPE_ENUM) != LUA_TTABLE) {\n\t\treturn 0;\n\t}\n\tif (lua_geti(L, -1, idx) != LUA_TSTRING) {\n\t\treturn 0;\n\t}\n\treturn 1;\n}\n\nstatic inline int\ntruetype_enum(lua_State *L, int idx, char buffer[], int buffer_sz) {\n\tlua_pushcfunction(L, lenum_fontname);\n\tlua_pushinteger(L, idx);\n\tif (lua_pcall(L, 1, 1, 0) != LUA_OK) {\n\t\tprintf(\"TRUETYPE_ENUM err: %s\\n\", lua_tostring(L, -1));\n\t\tlua_pop(L, 1);\n\t\treturn -1;\n\t}\n\tif (lua_isstring(L, -1)) {\n\t\tsize_t sz = 0;\n\t\tconst char *name = lua_tolstring(L, -1, &sz);\n\t\tif (sz >= buffer_sz) {\n\t\t\treturn (int)sz;\n\t\t}\n\t\tmemcpy(buffer, name, sz+1);\n\t\tlua_pop(L, 1);\n\t\treturn (int)sz;\n\t} else {\n\t\tlua_pop(L, 1);\n\t\treturn 0;\n\t}\n}\n\nstatic inline int\nimport_font(lua_State *L) {\n\tif (lua_getfield(L, LUA_REGISTRYINDEX, TRUETYPE_IMPORT) != LUA_TFUNCTION) {\n\t\treturn 0;\n\t}\n\tconst char * data = (const char *)lua_touserdata(L, 1);\n\tsize_t sz = lua_tointeger(L, 2);\n\tlua_pushlstring(L, data, sz);\n\tlua_call(L, 1, 0);\n\treturn 0;\n}\n\n\nstatic inline void\ntruetype_import(lua_State *L, void* fontdata, size_t sz) {\n\tlua_pushcfunction(L, import_font);\n\tlua_pushlightuserdata(L, fontdata);\n\tlua_pushinteger(L, sz);\n\tif (lua_pcall(L, 2, 0, 0) != LUA_OK) {\n\t\tprintf(\"TRUETYPE_IMPORT err: %s\\n\", lua_tostring(L, -1));\n\t\tlua_pop(L, 1);\n\t}\n}\n\n#endif\n"
  },
  {
    "path": "src/version.h",
    "content": "#ifndef soluna_version_h\n#define soluna_version_h\n\n#define SOLUNA_API_VERSION 3\n#ifndef SOLUNA_HASH_VERSION\n#define SOLUNA_HASH_VERSION \"dev\"\n#endif\n\n#endif\n"
  },
  {
    "path": "src/winfile.c",
    "content": "#include <stdio.h>\n\n#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)\n\n#include <windows.h>\n\nFILE *\nfopen_utf8(const char *filename, const char *mode) {\n\tWCHAR filenameW[FILENAME_MAX + 0x200 + 1];\n\tint n = MultiByteToWideChar(CP_UTF8,0,(const char*)filename,-1,filenameW,FILENAME_MAX + 0x200);\n\tif (n == 0)\n\t\treturn NULL;\n\tWCHAR modeW[128];\n\tn = MultiByteToWideChar(CP_UTF8,0,(const char*)mode,-1,modeW, 127);\n\tif (n == 0)\n\t\treturn NULL;\n\treturn _wfopen(filenameW, modeW);\n}\n\n#else\n\nFILE *\nfopen_utf8(const char *filename, const char *mode) {\n\treturn fopen(filename, mode);\n}\n\n#endif"
  },
  {
    "path": "src/writelog.c",
    "content": "#include <lua.h>\n#include <lauxlib.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <stdint.h>\n#include <time.h>\n#include <string.h>\n#include <ctype.h>\n#include \"loginfo.h\"\n\nstatic void\nwrite_timestamp(uint64_t ti) {\n\ttime_t timer = ti / 100;\n\tint msec = ti % 100;\n\tchar buffer[26];\n\tstruct tm* tm_info = localtime(&timer);\n    strftime(buffer, 26, \"%Y-%m-%d %H:%M:%S\", tm_info);\t\n\tprintf(\"[%s.%02d]\", buffer, msec);\n}\n\nstatic int\nlog_write_sokol(lua_State *L) {\n\tstatic const char *level[] = {\n\t\t\"PANIC\",\n\t\t\"ERROR\",\n\t\t\"WARN\",\n\t\t\"INFO\",\n\t};\n\tuint64_t ti = lua_tointeger(L, 1);\n\tstruct log_info *info = (struct log_info *)lua_touserdata(L, 2);\n\tif (info->log_level > 3)\n\t\tinfo->log_level = 3;\n\twrite_timestamp(ti);\n\tprintf(\"[%-5s]( %s:%d )\", level[info->log_level], info->tag, info->log_item );\n\tif (info->filename) {\n\t\tprintf(\" %s : (%d)\", info->filename, info->line_nr);\n\t}\n\tprintf(\" %s\\n\", info->message);\n\tfree(info);\n\treturn 0;\n}\n\nstatic int\nlog_write_ltask(lua_State *L) {\n\tuint64_t ti = lua_tointeger(L, 1);\n\tconst char *level = luaL_checkstring(L, 2);\n\tsize_t sz;\n\tconst char *msg = luaL_checklstring(L, 3, &sz);\n\tchar upper[6];\n\tint i;\n\tfor (i=0;i<5;i++) {\n\t\tupper[i] = toupper(level[i]);\n\t\tif (*level == 0)\n\t\t\tbreak;\n\t}\n\tupper[5] = 0;\n\twrite_timestamp(ti);\n\tif (strnlen(msg, sz+1) < sz) {\n\t\tprintf(\"[%-5s] \", upper);\n\t\tint i;\n\t\tfor (i=0;i<sz;i++) {\n\t\t\tunsigned char c = (unsigned char)msg[i];\n\t\t\tif (c < 32) {\n\t\t\t\tprintf(\"/%02X\", c);\n\t\t\t} else {\n\t\t\t\tprintf(\"%c\", c);\n\t\t\t}\n\t\t}\n\t\tprintf(\"\\n\");\n\t} else {\n\t\tprintf(\"[%-5s]%s\\n\", upper, msg );\n\t}\n\treturn 0;\n}\n\nint\nluaopen_applog(lua_State *L) {\n\tluaL_checkversion(L);\n\tluaL_Reg l[] = {\n\t\t{ \"sokol\", log_write_sokol },\n\t\t{ \"ltask\", log_write_ltask },\n\t\t{ NULL, NULL },\n\t};\n\tluaL_newlib(L, l);\n\treturn 1;\n}\n\n"
  },
  {
    "path": "src/yogaone.cpp",
    "content": "#include \"yoga/YGConfig.cpp\"\n#include \"yoga/YGEnums.cpp\"\n#include \"yoga/YGNode.cpp\"\n#include \"yoga/YGNodeLayout.cpp\"\n#include \"yoga/YGNodeStyle.cpp\"\n#include \"yoga/YGPixelGrid.cpp\"\n#include \"yoga/YGValue.cpp\"\n\n#include \"yoga/algorithm/AbsoluteLayout.cpp\"\n#include \"yoga/algorithm/Baseline.cpp\"\n#include \"yoga/algorithm/Cache.cpp\"\n#include \"yoga/algorithm/CalculateLayout.cpp\"\n#include \"yoga/algorithm/FlexLine.cpp\"\n#include \"yoga/algorithm/PixelGrid.cpp\"\n\n#include \"yoga/config/Config.cpp\"\n\n#include \"yoga/debug/AssertFatal.cpp\"\n\n#include \"yoga/debug/Log.cpp\"\n\n#include \"yoga/node/LayoutResults.cpp\"\n#include \"yoga/node/Node.cpp\"\n\n#define Node Node_\n#include \"yoga/event/event.cpp\"\n#undef Node\n"
  },
  {
    "path": "src/zipreader.h",
    "content": "#ifndef soluna_zip_reader_h\n#define soluna_zip_reader_h\n\n#include <stdint.h>\n\nstruct zipreader_name {\n\tconst char * zipfile;\n\tconst char * root;\n\tsize_t root_size;\n};\n\ntypedef void * zipreader_file;\n\nzipreader_file zipreader_open(struct zipreader_name *names, const char * filename);\nvoid zipreader_close(zipreader_file f);\nint zipreader_read(zipreader_file f, void *dst, int bytes);\nint zipreader_seek(zipreader_file f, int64_t offset, int origin);\nint64_t zipreader_tell(zipreader_file f);\nsize_t zipreader_size(zipreader_file f);\n\n#endif\n"
  },
  {
    "path": "test/audio.game",
    "content": "entry : test/audio.lua\n"
  },
  {
    "path": "test/audio.lua",
    "content": "local soluna = require \"soluna\"\nlocal matquad = require \"soluna.material.quad\"\nlocal mattext = require \"soluna.material.text\"\nlocal font = require \"soluna.font\"\nlocal file = require \"soluna.file\"\n\nsoluna.load_sounds \"asset/sounds.dl\"\nsoluna.set_window_title \"Soluna sound sample\"\n\nlocal args = ...\nlocal batch = args.batch\nlocal screen_w = args.width\nlocal screen_h = args.height\n\nlocal sound_bus = assert(soluna.audio_bus \"sound\")\nlocal music_bus = assert(soluna.audio_bus \"music\")\n\nlocal BUTTON_W <const> = 220\nlocal BUTTON_H <const> = 56\nlocal BUTTON_GAP <const> = 18\nlocal BUTTON_COLS <const> = 2\nlocal BUTTON_ROWS <const> = 5\nlocal SHADOW_Y <const> = 6\nlocal BUTTON_TEXT_COLOR <const> = 0xff28435c\nlocal STATUS_TEXT_COLOR <const> = 0xffdce7f2\n\nlocal BUTTON_PLAY_SHOT <const> = 1\nlocal BUTTON_TOGGLE_LOOP <const> = 2\nlocal BUTTON_STOP_LAST <const> = 3\nlocal BUTTON_STOP_LOOP <const> = 4\nlocal BUTTON_SEEK_START <const> = 5\nlocal BUTTON_SEEK_STEP <const> = 6\nlocal BUTTON_SOUND_DOWN <const> = 7\nlocal BUTTON_SOUND_UP <const> = 8\nlocal BUTTON_MUSIC_DOWN <const> = 9\nlocal BUTTON_MUSIC_UP <const> = 10\n\nlocal pointer_x = screen_w // 2\nlocal pointer_y = screen_h // 2\nlocal pressed = false\nlocal pressed_button\n\nlocal sound_volume = 1.0\nlocal music_volume = 1.0\nlocal last_voice\nlocal loop_voice\nlocal loop_time = 0.0\n\nlocal function load_font(data, name)\n\tif not data then\n\t\treturn\n\tend\n\tfont.import(data)\n\treturn font.name(name or \"\")\nend\n\nlocal function font_init()\n\tif soluna.platform == \"wasm\" then\n\t\tlocal fontid = load_font(file.load \"asset/font/SourceHanSansSC-Regular.ttf\", \"Source Han Sans SC Regular\")\n\t\tif fontid then\n\t\t\treturn fontid\n\t\tend\n\tend\n\n\tlocal sysfont = require \"soluna.font.system\"\n\tfor _, name in ipairs {\n\t\t\"WenQuanYi Micro Hei\",\n\t\t\"Microsoft YaHei\",\n\t\t\"Yuanti SC\",\n\t\t\"Source Han Sans SC Regular\",\n\t} do\n\t\tlocal ok, data = pcall(sysfont.ttfdata, name)\n\t\tlocal fontid = ok and load_font(data, name)\n\t\tif fontid then\n\t\t\treturn fontid\n\t\tend\n\tend\n\terror \"No available system font for audio sample\"\nend\n\nlocal fontid = font_init()\nlocal button_text = mattext.block(font.cobj(), fontid, 24, BUTTON_TEXT_COLOR, \"CV\")\nlocal info_text = mattext.block(font.cobj(), fontid, 20, STATUS_TEXT_COLOR, \"L\")\n\nlocal labels = {\n\t[BUTTON_PLAY_SHOT] = button_text(\"Play Shot\", BUTTON_W, BUTTON_H),\n\t[BUTTON_TOGGLE_LOOP] = button_text(\"Toggle Loop\", BUTTON_W, BUTTON_H),\n\t[BUTTON_STOP_LAST] = button_text(\"Stop Last Voice\", BUTTON_W, BUTTON_H),\n\t[BUTTON_STOP_LOOP] = button_text(\"Stop Loop Voice\", BUTTON_W, BUTTON_H),\n\t[BUTTON_SEEK_START] = button_text(\"Seek Loop 0.00\", BUTTON_W, BUTTON_H),\n\t[BUTTON_SEEK_STEP] = button_text(\"Seek Loop +0.20\", BUTTON_W, BUTTON_H),\n\t[BUTTON_SOUND_DOWN] = button_text(\"Sound -\", BUTTON_W, BUTTON_H),\n\t[BUTTON_SOUND_UP] = button_text(\"Sound +\", BUTTON_W, BUTTON_H),\n\t[BUTTON_MUSIC_DOWN] = button_text(\"Music -\", BUTTON_W, BUTTON_H),\n\t[BUTTON_MUSIC_UP] = button_text(\"Music +\", BUTTON_W, BUTTON_H),\n}\n\nlocal function clamp(v, lo, hi)\n\tif v < lo then\n\t\treturn lo\n\telseif v > hi then\n\t\treturn hi\n\tend\n\treturn v\nend\n\nlocal function playing(voice)\n\treturn voice ~= nil and voice:playing()\nend\n\nlocal function update_pointer(x, y)\n\tpointer_x = x\n\tpointer_y = y\nend\n\nlocal function button_rect(index)\n\tlocal total_w = BUTTON_COLS * BUTTON_W + (BUTTON_COLS - 1) * BUTTON_GAP\n\tlocal total_h = BUTTON_ROWS * BUTTON_H + (BUTTON_ROWS - 1) * BUTTON_GAP\n\tlocal origin_x = (screen_w - total_w) // 2\n\tlocal origin_y = (screen_h - total_h) // 2 - 40\n\tlocal col = (index - 1) % BUTTON_COLS\n\tlocal row = (index - 1) // BUTTON_COLS\n\treturn origin_x + col * (BUTTON_W + BUTTON_GAP), origin_y + row * (BUTTON_H + BUTTON_GAP)\nend\n\nlocal function button_at(x, y)\n\tfor i = 1, BUTTON_ROWS * BUTTON_COLS do\n\t\tlocal bx, by = button_rect(i)\n\t\tif x >= bx and x <= bx + BUTTON_W and y >= by and y <= by + BUTTON_H then\n\t\t\treturn i\n\t\tend\n\tend\nend\n\nlocal function click_button(index)\n\tif index == BUTTON_PLAY_SHOT then\n\t\tlast_voice = assert(soluna.play_sound(\"bloop\", {\n\t\t\tvolume = 0.25,\n\t\t\tpan = clamp(pointer_x / screen_w * 2.0 - 1.0, -1.0, 1.0),\n\t\t\tpitch = 0.95,\n\t\t}))\n\t\treturn\n\tend\n\n\tif index == BUTTON_TOGGLE_LOOP then\n\t\tlocal voice = loop_voice\n\t\tif playing(voice) then\n\t\t\tvoice:stop(0.1)\n\t\t\tif loop_voice == voice then\n\t\t\t\tloop_voice = nil\n\t\t\tend\n\t\telse\n\t\t\tloop_voice = assert(soluna.play_sound \"bloop_loop\")\n\t\tend\n\t\treturn\n\tend\n\n\tif index == BUTTON_STOP_LAST then\n\t\tlocal voice = last_voice\n\t\tif voice then\n\t\t\tvoice:stop()\n\t\t\tif last_voice == voice then\n\t\t\t\tlast_voice = nil\n\t\t\tend\n\t\tend\n\t\treturn\n\tend\n\n\tif index == BUTTON_STOP_LOOP then\n\t\tlocal voice = loop_voice\n\t\tif voice then\n\t\t\tvoice:stop(0.1)\n\t\t\tif loop_voice == voice then\n\t\t\t\tloop_voice = nil\n\t\t\t\tloop_time = 0.0\n\t\t\tend\n\t\tend\n\t\treturn\n\tend\n\n\tif index == BUTTON_SEEK_START then\n\t\tlocal voice = loop_voice\n\t\tif voice then\n\t\t\tvoice:seek(0.0)\n\t\tend\n\t\treturn\n\tend\n\n\tif index == BUTTON_SEEK_STEP then\n\t\tlocal voice = loop_voice\n\t\tif voice then\n\t\t\tlocal now = voice:tell() or 0.0\n\t\t\tvoice:seek(now + 0.2)\n\t\tend\n\t\treturn\n\tend\n\n\tif index == BUTTON_SOUND_DOWN then\n\t\tsound_volume = clamp(sound_volume - 0.1, 0.0, 1.0)\n\t\tsound_bus:set_volume(sound_volume)\n\t\treturn\n\tend\n\n\tif index == BUTTON_SOUND_UP then\n\t\tsound_volume = clamp(sound_volume + 0.1, 0.0, 1.0)\n\t\tsound_bus:set_volume(sound_volume)\n\t\treturn\n\tend\n\n\tif index == BUTTON_MUSIC_DOWN then\n\t\tmusic_volume = clamp(music_volume - 0.1, 0.0, 1.0)\n\t\tmusic_bus:set_volume(music_volume)\n\t\treturn\n\tend\n\n\tif index == BUTTON_MUSIC_UP then\n\t\tmusic_volume = clamp(music_volume + 0.1, 0.0, 1.0)\n\t\tmusic_bus:set_volume(music_volume)\n\tend\nend\n\nlocal callback = {}\n\nfunction callback.window_resize(w, h)\n\tscreen_w = w\n\tscreen_h = h\nend\n\nfunction callback.mouse_move(x, y)\n\tupdate_pointer(x, y)\nend\n\nfunction callback.mouse_button(button, key_state)\n\tif button ~= 0 then\n\t\treturn\n\tend\n\tif key_state == 1 then\n\t\tpressed_button = button_at(pointer_x, pointer_y)\n\t\tpressed = pressed_button ~= nil\n\t\treturn\n\tend\n\tlocal index = button_at(pointer_x, pointer_y)\n\tif pressed and index == pressed_button then\n\t\tclick_button(index)\n\tend\n\tpressed = false\n\tpressed_button = nil\nend\n\nfunction callback.touch_begin(x, y)\n\tupdate_pointer(x, y)\n\tpressed_button = button_at(x, y)\n\tpressed = pressed_button ~= nil\nend\n\nfunction callback.touch_moved(x, y)\n\tupdate_pointer(x, y)\n\tif button_at(x, y) ~= pressed_button then\n\t\tpressed = false\n\tend\nend\n\nfunction callback.touch_end(x, y)\n\tupdate_pointer(x, y)\n\tlocal index = button_at(x, y)\n\tif pressed and index == pressed_button then\n\t\tclick_button(index)\n\tend\n\tpressed = false\n\tpressed_button = nil\nend\n\nfunction callback.touch_cancelled()\n\tpressed = false\n\tpressed_button = nil\nend\n\nfunction callback.frame()\n\tlocal last = last_voice\n\tlocal loop = loop_voice\n\tlocal last_playing = playing(last)\n\tlocal loop_playing = playing(loop)\n\n\tif loop_playing then\n\t\tloop_time = loop:tell() or loop_time\n\telse\n\t\tloop_time = 0.0\n\tend\n\n\tlocal title = info_text(\"Audio API Sample\", 400, 28)\n\tlocal subtitle = info_text(\"Play voices, seek a stream voice, and adjust sound/music buses.\", 640, 24)\n\tlocal status_1 = info_text(\n\t\tstring.format(\"sound bus %.1f  |  music bus %.1f\", sound_volume, music_volume),\n\t\t480,\n\t\t24\n\t)\n\tlocal status_2 = info_text(\n\t\tstring.format(\n\t\t\t\"last voice %s  |  loop voice %s  |  loop time %.2f\",\n\t\t\tlast_playing and \"playing\" or \"idle\",\n\t\t\tloop_playing and \"playing\" or \"idle\",\n\t\t\tloop_time\n\t\t),\n\t\t640,\n\t\t24\n\t)\n\n\tbatch:add(title, (screen_w - 400) // 2, 40)\n\tbatch:add(subtitle, (screen_w - 640) // 2, 72)\n\tbatch:add(status_1, (screen_w - 480) // 2, screen_h - 90)\n\tbatch:add(status_2, (screen_w - 640) // 2, screen_h - 62)\n\n\tfor i = 1, BUTTON_ROWS * BUTTON_COLS do\n\t\tlocal bx, by = button_rect(i)\n\t\tlocal hovered = button_at(pointer_x, pointer_y) == i\n\t\tlocal active = pressed and pressed_button == i\n\t\tlocal face_y = by + (active and 4 or 0)\n\t\tlocal face_color = active and 0xffcfd8e4 or hovered and 0xfffbfdff or 0xffeef3f8\n\t\tbatch:add(matquad.quad(BUTTON_W, BUTTON_H, 0xff7389a3), bx, by + SHADOW_Y)\n\t\tbatch:add(matquad.quad(BUTTON_W, BUTTON_H, face_color), bx, face_y)\n\t\tbatch:add(labels[i], bx, face_y)\n\tend\nend\n\nreturn callback\n"
  },
  {
    "path": "test/bundle.lua",
    "content": "local sb = require \"soluna.spritebundle\"\n\nlocal filecache = setmetatable({ __missing = {} }, { __index = sb.loadimage })\n\nprint_r(sb.load(filecache, \"asset/sprites.dl\"))\n\nlocal ltask = require \"ltask\"\n\nlocal s = ltask.uniqueservice \"loader\"\nltask.call(s, \"init\", { max_sprite = 65536, texture_size = 1024 })\n\nlocal b = ltask.call(s, \"loadbundle\", \"asset/sprites.dl\")\nprint_r(b)"
  },
  {
    "path": "test/extlua/material/perspective_quad.lua",
    "content": "local render = require \"soluna.render\"\nlocal pqmat = require \"ext.material.perspective_quad\"\n\nlocal ctx = ...\nlocal state = ctx.state\n\npqmat.set_material_id(ctx.id)\n\nlocal inst_buffer = render.buffer {\n\ttype = \"vertex\",\n\tusage = \"stream\",\n\tlabel = \"extlua-perspective-quad-instance\",\n\tsize = pqmat.instance_size * ctx.settings.draw_instance,\n}\n\nlocal bindings = render.bindings()\nbindings:vbuffer(0, inst_buffer)\nbindings:sampler(0, state.default_sampler)\n\nlocal cobj = pqmat.new {\n\tinst_buffer = inst_buffer,\n\tbindings = bindings,\n\tuniform = state.uniform,\n\tsprite_bank = ctx.arg.bank_ptr,\n\ttmp_buffer = ctx.tmp_buffer,\n}\n\nlocal material = {}\n\nfunction material.reset()\n\tcobj:reset()\nend\n\nfunction material.submit(ptr, n)\n\tcobj:submit(ptr, n)\nend\n\nfunction material.draw(ptr, n, tex)\n\tbindings:view(1, state.views[tex + 1])\n\tcobj:draw(ptr, n, tex)\nend\n\nreturn material\n"
  },
  {
    "path": "test/extlua.game",
    "content": "entry : extlua.lua\nextlua_entry : extlua_init\nextlua_preload : sample\nextlua_material : perspective_quad\nextlua_material_path : extlua/material/?.lua\n"
  },
  {
    "path": "test/extlua.lua",
    "content": "-- bin/soluna.exe test/extlua.game\n\nlocal soluna = require \"soluna\"\nlocal foobar = require \"ext.foobar\"\nlocal matpq = require \"ext.material.perspective_quad\"\n\nprint(foobar.hello())\nsoluna.set_window_title \"extlua perspective quad\"\n\nlocal args = ...\nlocal batch = args.batch\nlocal callback = {}\n\nlocal CARD_W <const> = 160\nlocal CARD_H <const> = 196\nlocal HALF_W <const> = CARD_W * 0.5\nlocal HALF_H <const> = CARD_H * 0.5\nlocal WHITE <const> = 0xffffffff\n\nlocal function rgba(color)\n\tlocal a = color >> 24 & 0xff\n\tlocal r = color >> 16 & 0xff\n\tlocal g = color >> 8 & 0xff\n\tlocal b = color & 0xff\n\treturn string.pack(\"BBBB\", r, g, b, a)\nend\n\nlocal function create_canvas(width, height)\n\tlocal pixels = {}\n\tlocal clear = rgba(0)\n\tfor i = 1, width * height do\n\t\tpixels[i] = clear\n\tend\n\n\tlocal canvas = {}\n\n\tfunction canvas.set_pixel(x, y, color)\n\t\tif x < 0 or x >= width or y < 0 or y >= height then\n\t\t\treturn\n\t\tend\n\t\tpixels[y * width + x + 1] = rgba(color)\n\tend\n\n\tfunction canvas.to_content()\n\t\treturn table.concat(pixels)\n\tend\n\n\treturn canvas\nend\n\nlocal function make_card_sprite()\n\tlocal canvas = create_canvas(CARD_W, CARD_H)\n\n\tfor y = 0, CARD_H - 1 do\n\t\tfor x = 0, CARD_W - 1 do\n\t\t\tlocal r = 40 + x * 120 // (CARD_W - 1)\n\t\t\tlocal g = 56 + y * 140 // (CARD_H - 1)\n\t\t\tlocal b = 224 - y * 72 // (CARD_H - 1)\n\t\t\tcanvas.set_pixel(x, y, 0xff000000 | r << 16 | g << 8 | b)\n\t\tend\n\tend\n\n\tfor y = 0, CARD_H - 1 do\n\t\tfor x = 0, CARD_W - 1 do\n\t\t\tif x < 3 or x >= CARD_W - 3 or y < 3 or y >= CARD_H - 3 then\n\t\t\t\tcanvas.set_pixel(x, y, 0xffffffff)\n\t\t\telseif x % 32 == 0 or y % 32 == 0 then\n\t\t\t\tcanvas.set_pixel(x, y, 0x80ffffff)\n\t\t\tend\n\t\tend\n\tend\n\n\tsoluna.preload {\n\t\tfilename = \"@extlua_perspective_card\",\n\t\tcontent = canvas.to_content(),\n\t\tw = CARD_W,\n\t\th = CARD_H,\n\t}\n\n\treturn soluna.load_sprites {\n\t\t{\n\t\t\tname = \"card\",\n\t\t\tfilename = \"@extlua_perspective_card\",\n\t\t},\n\t}\nend\n\nlocal sprites = make_card_sprite()\nlocal card = assert(sprites.card)\n\nlocal function card_quad(theta)\n\tlocal dist = 460.0\n\tlocal focal = 460.0\n\tlocal c = math.cos(theta)\n\tlocal s = math.sin(theta)\n\tlocal corners = {\n\t\t{ -HALF_W, -HALF_H },\n\t\t{ HALF_W,  -HALF_H },\n\t\t{ -HALF_W, HALF_H },\n\t\t{ HALF_W,  HALF_H },\n\t}\n\n\tlocal quad = {}\n\tlocal q = {}\n\tfor i = 1, 4 do\n\t\tlocal x = corners[i][1]\n\t\tlocal y = corners[i][2]\n\t\tlocal rx = x * c\n\t\tlocal rz = -x * s\n\t\tlocal w = dist + rz\n\t\tlocal scale = focal / w\n\n\t\tquad[#quad + 1] = rx * scale\n\t\tquad[#quad + 1] = y * scale\n\t\tq[i] = 1.0 / w\n\tend\n\treturn quad, q\nend\n\nfunction callback.frame(count)\n\tlocal theta = math.sin(count * 0.021) * 1.15\n\tlocal quad, q = card_quad(theta)\n\tbatch:add(matpq.sprite(card, {\n\t\tquad = quad,\n\t\tq = q,\n\t\tcolor = WHITE,\n\t}), args.width * 0.5, args.height * 0.5)\nend\n\nreturn callback\n"
  },
  {
    "path": "test/file.lua",
    "content": "local file = require \"soluna.file\"\nlocal image = require \"soluna.image\"\nlocal lfs = require \"soluna.lfs\"\n\nprint_r(image.info(file.load \"asset/avatar.png\"))\nprint(lfs.realpath \".\")\n\nfor name in lfs.dir \".\" do\n\tprint_r(name, lfs.attributes(name))\nend\n"
  },
  {
    "path": "test/hello.game",
    "content": "entry : hello.lua\na.b : 1\na.c : 2\n"
  },
  {
    "path": "test/hello.lua",
    "content": "print \"Hello World\""
  },
  {
    "path": "test/icon.lua",
    "content": "local image = require \"soluna.image\"\nlocal file = require \"soluna.file\"\nlocal soluna = require \"soluna\"\n\nlocal c = file.load \"asset/lua-logo.png\"\nlocal content, w, h = image.load(c)\nsoluna.set_icon({ data = content, w = w, h = h })\n\nlocal callback = {}\n\nfunction callback.frame(count)\nend\n\nreturn callback\n"
  },
  {
    "path": "test/image.lua",
    "content": "local image = require \"soluna.image\"\nlocal file = require \"soluna.file\"\n\nlocal c = file.load \"asset/avatar.png\"\nprint(image.info(c))\nlocal content, w, h = image.load(c)\nlocal x, y, cw, ch = image.crop(content, w, h)\nlocal img = image.new(cw,ch)\nlocal src_rect = image.canvas(content, w, h, x, y, cw, ch)\nimage.blit(img:canvas(), src_rect)\n--img:write(\"crop.png\")\n\n"
  },
  {
    "path": "test/ime.lua",
    "content": "-- To run this sample:\n-- bin/soluna.exe entry=test/ime.lua\n\nlocal soluna = require \"soluna\"\nlocal app = require \"soluna.app\"\nlocal mattext = require \"soluna.material.text\"\nlocal matquad = require \"soluna.material.quad\"\nlocal matmask = require \"soluna.material.mask\"\nlocal font = require \"soluna.font\"\nlocal file = require \"soluna.file\"\nlocal utf8 = utf8\nlocal math = math\nlocal string = string\nlocal table = table\n\nlocal args = ...\nlocal batch = assert(args.batch)\n\nlocal KEY_LEFT <const> = 263\nlocal KEY_RIGHT <const> = 262\nlocal KEY_HOME <const> = 268\nlocal KEY_END <const> = 269\nlocal KEY_BACKSPACE <const> = 259\nlocal KEY_DEL <const> = 261\nlocal KEY_ENTER <const> = 257\nlocal KEYSTATE_PRESS <const> = 1\nlocal CHAR_BACKSPACE <const> = 8\nlocal CHAR_DELETE <const> = 127\n\nlocal FONT_SIZE <const> = 32\nlocal HELP_SIZE <const> = 18\nlocal BOX_WIDTH <const> = 720\nlocal BOX_HEIGHT <const> = 84\nlocal BOX_PADDING_X <const> = 18\nlocal BOX_PADDING_Y <const> = 8\nlocal BOX_RADIUS <const> = 10\nlocal CURSOR_BLINK <const> = 30\n\nlocal function load_font()\n\tif soluna.platform == \"wasm\" then\n\t\tlocal bundled_name = \"Source Han Sans SC Regular\"\n\t\tlocal bundled_path = \"asset/font/SourceHanSansSC-Regular.ttf\"\n\t\tlocal bundled_data = file.load(bundled_path)\n\t\tif bundled_data then\n\t\t\tfont.import(bundled_data)\n\t\t\tlocal bundled_id = font.name(bundled_name)\n\t\t\tif bundled_id then\n\t\t\t\treturn bundled_id, bundled_name\n\t\t\tend\n\t\tend\n\tend\n\n\tlocal sysfont = require \"soluna.font.system\"\n\tlocal candidates = {\n\t\t\"WenQuanYi Micro Hei\",  -- Linux\n\t\t\"Microsoft YaHei\",      -- Windows\n\t\t\"Yuanti SC\",            -- macOS\n\t\t\"Source Han Sans SC Regular\", -- WASM\n\t}\n\tfor _, name in ipairs(candidates) do\n\t\tlocal ok, data = pcall(sysfont.ttfdata, name)\n\t\tif ok and data then\n\t\t\tfont.import(data)\n\t\t\tlocal fontid = font.name(name)\n\t\t\tif fontid then\n\t\t\t\treturn fontid, name\n\t\t\tend\n\t\tend\n\tend\n\terror \"No available system font for IME sample\"\nend\n\nlocal function cache(f)\n\treturn setmetatable({}, {\n\t\t__index = function(self, k)\n\t\t\tlocal v = f(k)\n\t\t\tself[k] = v\n\t\t\treturn v\n\t\tend\n\t})\nend\n\nlocal quad_cache = cache(function(key)\n\tlocal w, h, c = key:match \"^(%-?%d+):(%-?%d+):(%x+)$\"\n\treturn matquad.quad(tonumber(w), tonumber(h), tonumber(c, 16))\nend)\n\nlocal function cached_quad(w, h, c)\n\tlocal key = string.format(\"%d:%d:%08x\", w, h, c)\n\treturn quad_cache[key]\nend\n\nlocal mask_cache = cache(function(key)\n\tlocal sprite, color = key:match \"^(%d+):(%x+)$\"\n\treturn matmask.mask(tonumber(sprite), tonumber(color, 16))\nend)\n\nlocal function cached_mask(sprite, color)\n\tlocal key = string.format(\"%d:%08x\", sprite, color)\n\treturn mask_cache[key]\nend\n\nlocal function clamp(v, lo, hi)\n\tif v < lo then\n\t\treturn lo\n\telseif v > hi then\n\t\treturn hi\n\tend\n\treturn v\nend\n\nlocal function rounded_box_rgba(w, h, radius)\n\tlocal r = math.floor(clamp(radius, 0, math.min(w, h) * 0.5))\n\tlocal edge = r * r\n\tlocal left = r\n\tlocal right = w - r\n\tlocal top = r\n\tlocal bottom = h - r\n\tlocal opaque = \"\\255\\255\\255\\255\"\n\tlocal transparent = \"\\255\\255\\255\\0\"\n\tlocal lines = {}\n\tfor y = 0, h - 1 do\n\t\tlocal py = y + 0.5\n\t\tlocal row = {}\n\t\tfor x = 0, w - 1 do\n\t\t\tlocal px = x + 0.5\n\t\t\tlocal qx = clamp(px, left, right)\n\t\t\tlocal qy = clamp(py, top, bottom)\n\t\t\tlocal dx = px - qx\n\t\t\tlocal dy = py - qy\n\t\t\trow[x + 1] = (dx * dx + dy * dy <= edge) and opaque or transparent\n\t\tend\n\t\tlines[y + 1] = table.concat(row)\n\tend\n\treturn table.concat(lines)\nend\n\nlocal rounded_box_cache = {}\n\nlocal function rounded_box_sprite(w, h, radius)\n\tlocal key = string.format(\"%d:%d:%d\", w, h, radius)\n\tlocal sprite = rounded_box_cache[key]\n\tif sprite then\n\t\treturn sprite\n\tend\n\tlocal filename = \"@\" .. \"ime_round_\" .. key:gsub(\":\", \"_\")\n\tsoluna.preload {\n\t\tfilename = filename,\n\t\tcontent = rounded_box_rgba(w, h, radius),\n\t\tw = w,\n\t\th = h,\n\t}\n\tlocal sprites = soluna.load_sprites {\n\t\t{\n\t\t\tname = \"box\",\n\t\t\tfilename = filename,\n\t\t\tcw = w,\n\t\t\tch = h,\n\t\t\tx = 0,\n\t\t\ty = 0,\n\t\t}\n\t}\n\trounded_box_cache[key] = sprites.box\n\treturn sprites.box\nend\n\nlocal fontid, font_name = load_font()\nlocal fontcobj = font.cobj()\nlocal text_block, text_cursor = mattext.block(fontcobj, fontid, FONT_SIZE, 0x000000, \"LV\")\nlocal help_block = mattext.block(fontcobj, fontid, HELP_SIZE, 0x222222, \"LV\")\n\nsoluna.set_window_title \"soluna ime sample\"\napp.set_ime_font(font_name, FONT_SIZE)\n\nlocal state = {\n\tscreen_w = args.width,\n\tscreen_h = args.height,\n\tmouse_x = 0,\n\tmouse_y = 0,\n\tfocused = true,\n\tcaret_tick = 0,\n\ttext = \"\",\n\tcursor = 0,\n\tsuppress_control_char = nil,\n}\n\nlocal function char_count(s)\n\treturn utf8.len(s) or 0\nend\n\nlocal function clamp_cursor()\n\tlocal n = char_count(state.text)\n\tif state.cursor < 0 then\n\t\tstate.cursor = 0\n\telseif state.cursor > n then\n\t\tstate.cursor = n\n\tend\nend\n\nlocal function byte_offset_for_char(index_1based)\n\treturn utf8.offset(state.text, index_1based) or (#state.text + 1)\nend\n\nlocal function insert_text(s)\n\tif not s or s == \"\" then\n\t\treturn\n\tend\n\tlocal byte = byte_offset_for_char(state.cursor + 1)\n\tstate.text = state.text:sub(1, byte - 1) .. s .. state.text:sub(byte)\n\tstate.cursor = state.cursor + char_count(s)\nend\n\nlocal function delete_backward()\n\tif state.cursor <= 0 then\n\t\treturn\n\tend\n\tlocal from = byte_offset_for_char(state.cursor)\n\tlocal to = byte_offset_for_char(state.cursor + 1)\n\tstate.text = state.text:sub(1, from - 1) .. state.text:sub(to)\n\tstate.cursor = state.cursor - 1\nend\n\nlocal function delete_forward()\n\tlocal n = char_count(state.text)\n\tif state.cursor >= n then\n\t\treturn\n\tend\n\tlocal from = byte_offset_for_char(state.cursor + 1)\n\tlocal to = byte_offset_for_char(state.cursor + 2)\n\tstate.text = state.text:sub(1, from - 1) .. state.text:sub(to)\nend\n\nlocal function is_control_char(codepoint)\n\treturn codepoint < 32 or (codepoint >= 127 and codepoint <= 159)\nend\n\nlocal function handle_control_delete(codepoint)\n\tif codepoint ~= CHAR_BACKSPACE and codepoint ~= CHAR_DELETE then\n\t\treturn\n\tend\n\tif state.suppress_control_char == codepoint then\n\t\tstate.suppress_control_char = nil\n\t\treturn\n\tend\n\tstate.suppress_control_char = nil\n\tif codepoint == CHAR_BACKSPACE then\n\t\tdelete_backward()\n\telse\n\t\tdelete_forward()\n\tend\nend\n\nlocal function decode_char_event(value)\n\tlocal t = type(value)\n\tif t == \"number\" then\n\t\tif is_control_char(value) then\n\t\t\treturn nil, value\n\t\tend\n\t\treturn utf8.char(value), nil\n\tend\n\tif t ~= \"string\" or value == \"\" then\n\t\treturn nil, nil\n\tend\n\tlocal first = utf8.codepoint(value, 1, 1)\n\tif first and is_control_char(first) then\n\t\treturn nil, first\n\tend\n\treturn value, nil\nend\n\nlocal function box_rect()\n\tlocal w = math.min(BOX_WIDTH, math.max(320, state.screen_w - 48))\n\tlocal h = BOX_HEIGHT\n\tlocal x = (state.screen_w - w) // 2\n\tlocal y = (state.screen_h - h) // 2\n\treturn x, y, w, h\nend\n\nlocal function in_box(x, y, bx, by, bw, bh)\n\treturn x >= bx and x <= bx + bw and y >= by and y <= by + bh\nend\n\nlocal callback = {}\n\nfunction callback.window_resize(w, h)\n\tstate.screen_w = w\n\tstate.screen_h = h\nend\n\nfunction callback.mouse_move(x, y)\n\tstate.mouse_x = x\n\tstate.mouse_y = y\nend\n\nfunction callback.mouse_button(button, key_state)\n\tif button ~= 0 or key_state ~= KEYSTATE_PRESS then\n\t\treturn\n\tend\n\tlocal bx, by, bw, bh = box_rect()\n\tstate.focused = in_box(state.mouse_x, state.mouse_y, bx, by, bw, bh)\n\tif not state.focused then\n\t\tapp.set_ime_rect(nil)\n\tend\nend\n\nfunction callback.char(value)\n\tif not state.focused then\n\t\treturn\n\tend\n\tlocal text_input, control = decode_char_event(value)\n\tif control then\n\t\thandle_control_delete(control)\n\t\treturn\n\tend\n\tif not text_input then\n\t\treturn\n\tend\n\tinsert_text(text_input)\n\tclamp_cursor()\n\tstate.caret_tick = 0\nend\n\nfunction callback.key(keycode, key_state)\n\tif key_state ~= KEYSTATE_PRESS or not state.focused then\n\t\treturn\n\tend\n\tif keycode == KEY_LEFT then\n\t\tstate.cursor = state.cursor - 1\n\telseif keycode == KEY_RIGHT then\n\t\tstate.cursor = state.cursor + 1\n\telseif keycode == KEY_HOME then\n\t\tstate.cursor = 0\n\telseif keycode == KEY_END then\n\t\tstate.cursor = char_count(state.text)\n\telseif keycode == KEY_BACKSPACE then\n\t\tdelete_backward()\n\t\tstate.suppress_control_char = CHAR_BACKSPACE\n\telseif keycode == KEY_DEL then\n\t\tdelete_forward()\n\t\tstate.suppress_control_char = CHAR_DELETE\n\telseif keycode == KEY_ENTER then\n\t\tinsert_text \"\\n\"\n\tend\n\tclamp_cursor()\n\tstate.caret_tick = 0\nend\n\nfunction callback.frame()\n\tclamp_cursor()\n\tlocal bx, by, bw, bh = box_rect()\n\tlocal box_sprite = rounded_box_sprite(bw, bh, BOX_RADIUS)\n\tlocal tx = bx + BOX_PADDING_X\n\tlocal ty = by + BOX_PADDING_Y\n\tlocal tw = bw - BOX_PADDING_X * 2\n\tlocal th = bh - BOX_PADDING_Y * 2\n\n\tbatch:add(cached_quad(state.screen_w, state.screen_h, 0xf2f2f2ff), 0, 0)\n\tbatch:add(cached_mask(box_sprite, state.focused and 0xffffffff or 0xe8e8e8ff), bx, by)\n\tbatch:add(\n\t\tcached_quad(\n\t\t\tmath.max(bw - BOX_RADIUS * 2, 2),\n\t\t\t2,\n\t\t\tstate.focused and 0x1d6ef0ff or 0x9a9a9aff\n\t\t),\n\t\tbx + BOX_RADIUS,\n\t\tby + bh - 2\n\t)\n\n\tlocal label = text_block(state.text, tw, th)\n\tbatch:add(label, tx, ty)\n\n\tlocal cx, cy, cw, ch, n, descent = text_cursor(state.text, state.cursor, tw, th)\n\tstate.cursor = n\n\tdescent = descent or 0\n\tif state.focused then\n\t\tapp.set_ime_rect {\n\t\t\tx = tx + cx,\n\t\t\ty = ty + cy - descent,\n\t\t\twidth = cw,\n\t\t\theight = ch,\n\t\t\ttext_color = 0xff000000,\n\t\t}\n\telse\n\t\tapp.set_ime_rect(nil)\n\tend\n\n\tstate.caret_tick = (state.caret_tick + 1) % (CURSOR_BLINK * 2)\n\tif state.focused and state.caret_tick < CURSOR_BLINK then\n\t\tbatch:add(cached_quad(math.max(cw, 2), ch, 0x111111ff), tx + cx, ty + cy)\n\tend\n\n\tlocal help = help_block(\"Click box, type with CJK.\", state.screen_w - 32, 24)\n\tbatch:add(help, 16, 16)\nend\n\nreturn callback\n"
  },
  {
    "path": "test/intersect.lua",
    "content": "local soluna = require \"soluna\"\nlocal quad = require \"soluna.material.quad\"\n\nlocal args = ...\nlocal batch = args.batch\n\nlocal callback = {}\n\nlocal mx = 0\nlocal my = 0\n\nfunction callback.mouse_move(x, y)\n\tmx, my = x, y\nend\n\nfunction callback.frame(count)\n\tlocal rad = count * math.pi / 180\n\t-- scale x2, move to the center of screen\n\tbatch:layer(2, args.width / 2, args.height / 2)\n\t\t-- rotate canvas\n\t\tbatch:layer(rad)\n\t\t\t-- use (-50, -50) , center of quad (100,100) as original point\n\t\t\tbatch:layer(-50, -50)\n\t\t\t\tlocal x, y = batch:point(mx, my)\n\t\t\t\tlocal color = 0xffffff\n\t\t\t\tif x >=0 and x < 100 and y >=0 and y < 100 then\n\t\t\t\t\tcolor = 0xff0000\n\t\t\t\tend\n\t\t\t\tbatch:add(quad.quad(100,100,color))\n\t\t\tbatch:layer()\n\t\tbatch:layer()\n\tbatch:layer()\nend\n\nreturn callback\n\n"
  },
  {
    "path": "test/layout.lua",
    "content": "local layout = require \"soluna.layout\"\nlocal datalist = require \"soluna.datalist\"\nlocal matquad = require \"soluna.material.quad\"\n\nlocal args = ...\n\nlocal hud = [[\nid : screen\npadding : 10\ndirection : row\ngap : 10\nleft :\n\twidth : 400\n\tbackground : 0x40000000\nright :\n\tflex : 1\n\tgap : 10\n\tnode :\n\t\tflex : 0.7\n\t\tbackground : 0x40ffffff\n\tnode :\n\t\tflex : 0.3\n\t\tbackground : 0x40ffffff\n]]\n\nlocal dom = layout.load(datalist.parse_list(hud))\nlocal screen = dom.screen\n\nlocal function calc_hub()\n\tscreen.width = args.width\n\tscreen.height = args.height\n\treturn layout.calc(dom)\nend\n\nlocal draw_list = calc_hub()\n\nlocal function draw_hud()\n\tfor _, obj in ipairs(draw_list) do\n\t\targs.batch:add(matquad.quad(obj.w, obj.h, obj.background), obj.x, obj.y)\n\tend\nend\n\nlocal callback = {}\n\nfunction callback.frame(count)\n\tdraw_hud()\nend\n\nfunction callback.window_resize(w,h)\n\targs.width = w\n\targs.height = h\n\tdraw_list = calc_hub()\nend\n\nreturn callback\n\n"
  },
  {
    "path": "test/mask.lua",
    "content": "-- To run this sample :\n-- bin/soluna.exe entry=test/sprite.lua\nlocal soluna = require \"soluna\"\nlocal ltask = require \"ltask\"\nlocal mask = require \"soluna.material.mask\"\n\nsoluna.set_window_title \"soluna sprite sample\"\nlocal sprites = soluna.load_sprites \"asset/sprites.dl\"\n\nlocal args = ...\nlocal batch = args.batch\n\nlocal callback = {}\n\nfunction callback.frame(count)\n\tbatch:add(mask.mask(sprites.avatar, 0x40000000), args.width / 2, args.height/2, 1, 0)\nend\n\nreturn callback\n\n"
  },
  {
    "path": "test/mtex.game",
    "content": "# a small texture size for testing\ntexture_size : 64\nentry : mtex.lua\n"
  },
  {
    "path": "test/mtex.lua",
    "content": "-- To run this sample :\n-- bin/soluna.exe test/mtex.game\nlocal soluna = require \"soluna\"\nlocal ltask = require \"ltask\"\n\nsoluna.set_window_title \"multiple texture\"\n\nlocal bundle = {}\n\nlocal function color(r,g,b)\n\tlocal name = string.format(\"%x%x%x\", r,g,b)\n\tbundle[#bundle+1] = {\n\t\tname = r << 8 | g << 4 | b,\n\t\tfilename = \"@\" .. name,\n\t}\n\treturn {\n\t\tfilename = \"@\" .. name,\n\t\tcontent = string.pack(\"BBBB\", r << 4, g << 4, b << 4, 255),\n\t\tw = 1,\n\t\th = 1,\n\t}\nend\n\nlocal function colors()\n\tlocal results = {}\n\tlocal n = 1\n\tfor r = 0, 15 do\n\t\tfor g = 0, 15 do\n\t\t\tfor b = 0, 15 do\n\t\t\t\tresults[n] = color(r,g,b)\n\t\t\t\tn = n + 1\n\t\t\tend\n\t\tend\n\tend\n\treturn results\nend\n\nsoluna.preload(colors())\n\nlocal rects = soluna.load_sprites(bundle)\n\nlocal args = ...\nlocal batch = args.batch\n\nlocal callback = {}\n\nfunction callback.frame(count)\n\tbatch:layer(10, 100, 100)\n\tfor i = 0, 63 do\n\t\tfor j = 0,63 do\n\t\t\tbatch:add(rects[i * 64 + j], i, j)\n\t\tend\n\tend\n\tbatch:layer()\nend\n\nreturn callback\n\n"
  },
  {
    "path": "test/setting.lua",
    "content": "local soluna = require \"soluna\"\n\nprint_r(soluna.settings())\n"
  },
  {
    "path": "test/sprite.lua",
    "content": "-- To run this sample :\n-- bin/soluna.exe entry=test/sprite.lua\nlocal soluna = require \"soluna\"\nlocal ltask = require \"ltask\"\n\nsoluna.set_window_title \"soluna sprite sample\"\nlocal sprites = soluna.load_sprites \"asset/sprites.dl\"\n\nsoluna.preload {\n\t{\n\t\tfilename = \"@red\",\n\t\tcontent = \"\\xff\\0\\0\\xff\",\n\t\tw = 1,\n\t\th = 1,\n\t},\n\t{\n\t\tfilename = \"@green\",\n\t\tcontent = \"\\0\\xff\\0\\xff\",\n\t\tw = 1,\n\t\th = 1,\n\t},\n}\n\nlocal rects = soluna.load_sprites {\n\t{\n\t\tname = \"red\",\n\t\tfilename = \"@red\",\n\t},\n\t{\n\t\tname = \"green\",\n\t\tfilename = \"@green\",\n\t}\n}\n\nlocal args = ...\nlocal batch = args.batch\n\nlocal callback = {}\nlocal rot = 0\nlocal delta = math.rad(1)\nfunction callback.frame(count)\n\tbatch:layer(100, args.width/2 , args.height/2)\n\tbatch:layer(rot)\n\tbatch:add(rects.red)\n\tbatch:layer()\n\tbatch:layer(-rot)\n\tbatch:add(rects.green)\n\tbatch:layer()\n\tbatch:layer()\n\trot = rot + delta\n\tbatch:add(sprites.avatar, args.width / 2, args.height/2)\nend\n\nreturn callback\n\n"
  },
  {
    "path": "test/spritepack.lua",
    "content": "local spritemgr = require \"soluna.spritemgr\"\n\n-- texture size = 128\nlocal bank = spritemgr.newbank(65536, 128)\n\nbank:add(32, 16)\nbank:add(64, 32)\nbank:add(96, 96)\nbank:add(96, 96)\n\nlocal texid, n = bank:pack()\nprint(\"Pack\",n,\"from\",texid)\n\nfor i = 1, n do\n\tlocal r = bank:altas(texid + i - 1)\n\tfor k,v in pairs(r) do\n\t\tr[k] = { x = v >> 32, y = v & 0xffffffff }\n\tend\n\tprint_r(i, r)\nend\n\n"
  },
  {
    "path": "test/test.lua",
    "content": "print_r {\"Hello World\"}\n"
  },
  {
    "path": "test/text.lua",
    "content": "local soluna = require \"soluna\"\nlocal ltask = require \"ltask\"\nlocal mattext = require \"soluna.material.text\"\nlocal matquad = require \"soluna.material.quad\"\nlocal font = require \"soluna.font\"\nlocal file = require \"soluna.file\"\n\nlocal function font_init()\n\tif soluna.platform == \"wasm\" then\n\t\tlocal bundled_path = \"asset/font/SourceHanSansSC-Regular.ttf\"\n\t\tlocal bundled_data = file.load(bundled_path)\n\t\tif bundled_data then\n\t\t\tfont.import(bundled_data)\n\t\t\tlocal bundled_id = font.name \"Source Han Sans SC Regular\"\n\t\t\tif bundled_id then\n\t\t\t\treturn bundled_id\n\t\t\tend\n\t\tend\n\tend\n\n\tlocal sysfont = require \"soluna.font.system\"\n\tlocal candidates = {\n\t\t\"WenQuanYi Micro Hei\",    -- Linux\n\t\t\"Microsoft YaHei\",        -- Windows\n\t\t\"Yuanti SC\",              -- macOS\n\t\t\"Source Han Sans SC Regular\", -- WASM\n\t}\n\tfor _, name in ipairs(candidates) do\n\t\tlocal ok, data = pcall(sysfont.ttfdata, name)\n\t\tif ok and data then\n\t\t\tfont.import(data)\n\t\t\tlocal fontid = font.name(name)\n\t\t\tif fontid then\n\t\t\t\treturn fontid\n\t\t\tend\n\t\tend\n\tend\n\terror \"No available system font for text sample\"\nend\n\nsoluna.set_window_title \"soluna text sample\"\n\nlocal args = ...\nlocal batch = args.batch\nlocal fontid = font_init()\nlocal fontcobj = font.cobj()\n\nlocal callback = {}\nlocal WIDTH <const> = 200\nlocal HEIGHT <const> = 200\nlocal screen_w = args.width\nlocal screen_h = args.height\n\nfunction callback.window_resize(w, h)\n\tscreen_w = w\n\tscreen_h = h\nend\n\nlocal TEXT <const> = \"Hello, 这是一条很长的句子。它会在文本区居中。\"\n-- size 32; color 0; alignment center\nlocal block, cursor = mattext.block(fontcobj, fontid, 32, 0, \"CV\")\nlocal label = block(TEXT, WIDTH, HEIGHT)\n\nlocal CURSOR_N = 0\n\nfunction callback.frame(count)\n\tlocal x = (screen_w - WIDTH) / 2\n\tlocal y = (screen_h - HEIGHT) / 2\n\tbatch:add(matquad.quad(WIDTH, HEIGHT, 0x400000ff), x, y)\n\tbatch:add(label, x, y)\n\t-- cursor\n\tlocal cx, cy, cw, ch, n = cursor(TEXT, CURSOR_N, WIDTH, HEIGHT)\n\tCURSOR_N = n\n\tbatch:add(matquad.quad(cw, ch, 0xffffff), cx + x, cy + y)\nend\n\nfunction callback.key(keycode, state)\n\tif state == 1 then         -- press\n\t\tif keycode == 262 then -- right\n\t\t\tCURSOR_N = CURSOR_N + 1\n\t\telseif keycode == 263 then -- left\n\t\t\tCURSOR_N = CURSOR_N - 1\n\t\telse\n\t\t\tprint(keycode)\n\t\tend\n\tend\nend\n\nreturn callback\n"
  },
  {
    "path": "test/version.lua",
    "content": "local soluna = require \"soluna\"\nprint (soluna.version)"
  },
  {
    "path": "test/window.game",
    "content": "entry : window.lua\n"
  },
  {
    "path": "test/window.lua",
    "content": "-- To run this sample :\n-- bin/soluna.exe entry=test/window.lua\n-- bin/soluna.exe test/window.game\nlocal soluna = require \"soluna\"\n\nsoluna.set_window_title \"Soluna Sample\"\n\nlocal callback = {}\n\nfunction callback.frame(count)\nend\n\nreturn callback\n\n"
  },
  {
    "path": "website/README.md",
    "content": "# Website\n\n[中文](./README.zh-CN.md)\n\nThis directory contains the Astro-based website for Soluna Live Examples.\n\nIt is responsible for:\n\n- rendering the website pages\n- generating API documentation pages from `../docs/`\n- generating online example pages from `../test/`\n- using the repository `../README.md` as the homepage content\n- packaging `asset.zip` from `../asset/`\n\nThe core WebAssembly runtime is not built here. `soluna.js`, `soluna.wasm`,\nand `sample.wasm` are produced by `luamake` from the repository root.\n\n## Local development\n\nBuild the web runtime from the root:\n\n```bash\nluamake -compiler emcc\nluamake -compiler emcc sample\n```\n\nThen start the website from the `website/` directory:\n\n```bash\npnpm install\npnpm run dev\n```\n"
  },
  {
    "path": "website/README.zh-CN.md",
    "content": "# Website\n\n[English](./README.md)\n\n这个目录包含 Soluna 在线示例网站的 Astro 源码。\n\n它负责：\n\n- 渲染网站页面\n- 从 `../docs/` 生成 API 文档页面\n- 从 `../test/` 生成在线示例页面\n- 使用仓库根目录的 `../README.md` 作为首页内容\n- 从 `../asset/` 打包 `asset.zip`\n\n核心 WebAssembly 运行时并不在这里构建。`soluna.js`、`soluna.wasm`\n和 `sample.wasm` 由仓库根目录的 `luamake` 生成。\n\n## 本地开发\n\n先在仓库根目录构建 Web 运行时：\n\n```bash\nluamake -compiler emcc\nluamake -compiler emcc sample\n```\n\n然后进入 `website/` 目录启动站点：\n\n```bash\npnpm install\npnpm run dev\n```\n"
  },
  {
    "path": "website/astro.config.mjs",
    "content": "import process from 'node:process'\nimport { defineConfig } from 'astro/config'\n\nconst site = process.env.SITE_URL || 'https://cloudwu.github.io'\nconst configuredBase = process.env.SITE_BASE || '/'\nconst base = configuredBase.endsWith('/') ? configuredBase : `${configuredBase}/`\n\nexport default defineConfig({\n  site,\n  base,\n  output: 'static',\n})\n"
  },
  {
    "path": "website/eslint.config.mjs",
    "content": "import antfu from '@antfu/eslint-config'\n\nexport default antfu(\n  {\n    astro: true,\n    typescript: true,\n    stylistic: {\n      semi: false,\n    },\n  },\n  {\n    rules: {\n      'style/semi': ['error', 'never'],\n    },\n  },\n)\n"
  },
  {
    "path": "website/package.json",
    "content": "{\n  \"name\": \"website\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"packageManager\": \"pnpm@10.28.2\",\n  \"scripts\": {\n    \"prepare:runtime\": \"node scripts/prepare-runtime.mjs\",\n    \"prepare:runtime:debug\": \"SOLUNA_MODE=debug node scripts/prepare-runtime.mjs\",\n    \"dev\": \"pnpm run prepare:runtime && astro dev\",\n    \"build\": \"pnpm run prepare:runtime && astro build\",\n    \"build:pages\": \"SITE_BASE=/soluna/ pnpm run build\",\n    \"lint\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n    \"format\": \"eslint . --fix\",\n    \"preview\": \"astro preview\"\n  },\n  \"dependencies\": {\n    \"astro\": \"^5.16.8\",\n    \"astro-theme-soluna\": \"workspace:*\",\n    \"marked\": \"^15.0.12\"\n  },\n  \"devDependencies\": {\n    \"@antfu/eslint-config\": \"^4.19.0\",\n    \"eslint\": \"^9.39.1\",\n    \"eslint-plugin-astro\": \"^1.6.0\"\n  }\n}\n"
  },
  {
    "path": "website/packages/astro-theme-soluna/package.json",
    "content": "{\n  \"name\": \"astro-theme-soluna\",\n  \"type\": \"module\",\n  \"version\": \"0.1.0\",\n  \"exports\": {\n    \"./layouts/BaseLayout.astro\": \"./src/layouts/BaseLayout.astro\",\n    \"./components/Footer.astro\": \"./src/components/Footer.astro\",\n    \"./components/Hero.astro\": \"./src/components/Hero.astro\",\n    \"./components/Menubar.astro\": \"./src/components/Menubar.astro\",\n    \"./components/Nav.astro\": \"./src/components/Nav.astro\",\n    \"./components/PlainList.astro\": \"./src/components/PlainList.astro\",\n    \"./components/Section.astro\": \"./src/components/Section.astro\",\n    \"./components/docs/DocsPage.astro\": \"./src/components/docs/DocsPage.astro\",\n    \"./components/examples/ExampleListPage.astro\": \"./src/components/examples/ExampleListPage.astro\",\n    \"./components/examples/ExamplePlayPage.astro\": \"./src/components/examples/ExamplePlayPage.astro\",\n    \"./client/play\": \"./src/client/play.ts\"\n  },\n  \"peerDependencies\": {\n    \"astro\": \"^5.16.8\"\n  },\n  \"dependencies\": {\n    \"fflate\": \"^0.8.2\"\n  }\n}\n"
  },
  {
    "path": "website/packages/astro-theme-soluna/src/client/play.ts",
    "content": "import { strToU8, zipSync } from 'fflate'\n\ninterface RuntimeModule {\n  FS: {\n    writeFile: (path: string, data: Uint8Array, options?: { canOwn?: boolean }) => void\n  }\n  FS_createPath: (root: string, path: string, canRead: boolean, canWrite: boolean) => void\n  _soluna_runtime_quit?: () => void\n}\n\ninterface PlayAppOptions {\n  appFactory: (options: Record<string, unknown>) => Promise<RuntimeModule>\n  appBaseUrl: string\n  canvas: HTMLCanvasElement\n  print: (text: string) => void\n  printErr: (text: string) => void\n  onAbort: (reason: unknown) => void\n}\n\ninterface StartOptions {\n  arguments: string[]\n  files: Array<{\n    path: string\n    data: Uint8Array\n    canOwn?: boolean\n  }>\n}\n\ninterface PlayOptions {\n  exampleSource: string\n  exampleGameSettings?: string\n  exampleRuntimeFiles?: RuntimeFile[]\n}\n\ninterface RuntimeHandle {\n  stop: () => void\n}\n\ninterface RuntimeFile {\n  path: string\n  source: string\n}\n\ndeclare global {\n  interface Window {\n    SOLUNA_PLAY_ACTIVE?: RuntimeHandle\n  }\n}\n\nfunction qs<T extends Element>(selector: string, root: ParentNode = document): T | null {\n  return root.querySelector<T>(selector)\n}\n\nfunction normalizeBaseUrl(baseUrl: string): URL {\n  const normalized = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`\n  return new URL(normalized, window.location.href)\n}\n\nfunction normalizeFileData(data: Uint8Array | ArrayBuffer | ArrayBufferView): Uint8Array {\n  if (data instanceof Uint8Array) {\n    return data\n  }\n  if (data instanceof ArrayBuffer) {\n    return new Uint8Array(data)\n  }\n  return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)\n}\n\nfunction ensureAbsolutePath(path: string): string {\n  if (!path.startsWith('/')) {\n    throw new TypeError(`Expected an absolute FS path, got: ${path}`)\n  }\n  return path\n}\n\nfunction dirname(path: string): string {\n  const normalized = ensureAbsolutePath(path)\n  const index = normalized.lastIndexOf('/')\n  if (index <= 0) {\n    return '/'\n  }\n  return normalized.slice(0, index)\n}\n\nfunction ensureParentDirectory(runtimeModule: RuntimeModule, path: string): void {\n  const dir = dirname(path)\n  if (dir === '/') {\n    return\n  }\n  runtimeModule.FS_createPath('/', dir.slice(1), true, true)\n}\n\nasync function fetchArrayBuffer(url: string): Promise<ArrayBuffer> {\n  const response = await fetch(url)\n  if (!response.ok) {\n    throw new Error(`Failed to load ${url}`)\n  }\n  return response.arrayBuffer()\n}\n\nasync function ensureCrossOriginIsolation(serviceWorkerUrl: string): Promise<boolean> {\n  if (window.crossOriginIsolated) {\n    return true\n  }\n  if (!('serviceWorker' in navigator)) {\n    return false\n  }\n\n  await navigator.serviceWorker.register(serviceWorkerUrl)\n  if (!navigator.serviceWorker.controller) {\n    window.location.reload()\n    return false\n  }\n  return true\n}\n\nfunction installRuntimeFiles(runtimeModule: RuntimeModule, files: StartOptions['files']): void {\n  files.forEach((file) => {\n    ensureParentDirectory(runtimeModule, file.path)\n    runtimeModule.FS.writeFile(file.path, file.data, { canOwn: file.canOwn })\n  })\n}\n\nfunction resolveQuitApp(instance: RuntimeModule): (() => void) | undefined {\n  if (typeof instance._soluna_runtime_quit === 'function') {\n    return () => {\n      instance._soluna_runtime_quit?.()\n    }\n  }\n}\n\nasync function createRuntimeHandle(\n  playOptions: PlayAppOptions,\n  startOptions: StartOptions,\n): Promise<RuntimeHandle> {\n  const appBaseUrl = normalizeBaseUrl(playOptions.appBaseUrl)\n  const instance = await playOptions.appFactory({\n    arguments: startOptions.arguments,\n    canvas: playOptions.canvas,\n    print: playOptions.print,\n    printErr: playOptions.printErr,\n    locateFile(path: string) {\n      return new URL(path, appBaseUrl).toString()\n    },\n    preRun: [\n      (runtimeModule: RuntimeModule) => {\n        installRuntimeFiles(runtimeModule, startOptions.files)\n      },\n    ],\n    onAbort: (reason: unknown) => {\n      playOptions.onAbort(reason)\n    },\n  })\n\n  const quitApp = resolveQuitApp(instance)\n  let stopped = false\n\n  return {\n    stop() {\n      if (stopped) {\n        return\n      }\n      stopped = true\n\n      if (quitApp) {\n        try {\n          quitApp()\n        }\n        catch {\n          // Ignore quit failures during teardown.\n        }\n      }\n    },\n  }\n}\n\nfunction setStatus(text: string): void {\n  const status = qs<HTMLElement>('#play-status')\n  if (status) {\n    status.textContent = text\n  }\n}\n\nfunction setNote(text: string): void {\n  const note = qs<HTMLElement>('#play-note')\n  if (note) {\n    note.textContent = text\n  }\n}\n\nfunction setOverlayVisible(visible: boolean): void {\n  const overlay = qs<HTMLElement>('#play-overlay')\n  if (overlay) {\n    overlay.classList.toggle('hidden', !visible)\n  }\n}\n\nfunction resetConsole(): void {\n  const consoleTarget = qs<HTMLElement>('#console-output')\n  if (consoleTarget) {\n    consoleTarget.textContent = ''\n  }\n}\n\nfunction appendConsole(text: string, isError: boolean): void {\n  const consoleTarget = qs<HTMLElement>('#console-output')\n  if (!consoleTarget) {\n    return\n  }\n\n  const line = document.createElement('div')\n  line.textContent = text\n  if (isError) {\n    line.className = 'console-error'\n  }\n  consoleTarget.appendChild(line)\n  consoleTarget.scrollTop = consoleTarget.scrollHeight\n}\n\nfunction createCanvas(): HTMLCanvasElement {\n  const host = qs<HTMLElement>('#soluna-stage-host')\n  if (!host) {\n    throw new Error('Missing #soluna-stage-host.')\n  }\n\n  host.replaceChildren()\n  const canvas = document.createElement('canvas')\n  canvas.id = 'soluna-canvas'\n  host.appendChild(canvas)\n  return canvas\n}\n\nfunction setupCanvasResize(canvas: HTMLCanvasElement): () => void {\n  const resize = () => {\n    const rect = canvas.getBoundingClientRect()\n    const ratio = window.devicePixelRatio || 1\n    canvas.width = Math.max(1, Math.floor(rect.width * ratio))\n    canvas.height = Math.max(1, Math.floor(rect.height * ratio))\n  }\n  resize()\n  return resize\n}\n\nfunction buildMainGame(exampleGameSettings = ''): string {\n  const lines = [\n    'entry : main.lua',\n    'high_dpi : true',\n    'text_sampler :',\n    '  min_filter : linear',\n    '  mag_filter : linear',\n    'extlua_entry : extlua_init',\n    'extlua_preload : sample',\n  ]\n  const settings = exampleGameSettings.trim()\n  if (settings) {\n    lines.push(settings)\n  }\n  lines.push('')\n  return lines.join('\\n')\n}\n\nfunction normalizeRuntimeFilePath(path: string): string {\n  if (path.startsWith('/') || path.includes('\\\\')) {\n    throw new Error(`Invalid runtime file path: ${path}`)\n  }\n  const parts = path.split('/')\n  if (parts.some(part => part === '' || part === '.' || part === '..')) {\n    throw new Error(`Invalid runtime file path: ${path}`)\n  }\n  return path\n}\n\nfunction buildMainZip(exampleSource: string, exampleGameSettings: string, runtimeFiles: RuntimeFile[]): Uint8Array {\n  const entries: Record<string, Uint8Array> = {\n    'main.lua': strToU8(exampleSource),\n    'main.game': strToU8(buildMainGame(exampleGameSettings)),\n  }\n  runtimeFiles.forEach((file) => {\n    const runtimePath = normalizeRuntimeFilePath(file.path)\n    if (runtimePath === 'main.lua' || runtimePath === 'main.game') {\n      throw new Error(`Reserved runtime file path: ${runtimePath}`)\n    }\n    entries[runtimePath] = strToU8(file.source)\n  })\n  return zipSync(entries)\n}\n\nasync function destroyActiveRuntime(): Promise<void> {\n  const runtime = window.SOLUNA_PLAY_ACTIVE\n  window.SOLUNA_PLAY_ACTIVE = undefined\n  runtime?.stop()\n}\n\nasync function loadAppFactory(basePath: string): Promise<PlayAppOptions['appFactory']> {\n  const runtimeUrl = new URL(`${basePath}runtime/soluna.js`, window.location.href).href\n  const runtimeApi = await import(/* @vite-ignore */ runtimeUrl)\n  if (typeof runtimeApi.default !== 'function') {\n    throw new TypeError('soluna.js does not export createApp.')\n  }\n  return runtimeApi.default as PlayAppOptions['appFactory']\n}\n\nasync function ensureIsolation(basePath: string): Promise<boolean> {\n  if (window.crossOriginIsolated) {\n    return true\n  }\n  if (!('serviceWorker' in navigator)) {\n    setStatus('Cross-origin isolation required.')\n    setNote('Service worker is unavailable on this browser.')\n    return false\n  }\n\n  try {\n    const isolated = await ensureCrossOriginIsolation(`${basePath}coi-serviceworker.min.js`)\n    if (!isolated) {\n      setStatus('Reloading for cross-origin isolation...')\n    }\n    return isolated\n  }\n  catch (error) {\n    setStatus('Failed to register COI service worker.')\n    setNote(error instanceof Error ? error.message : String(error))\n    return false\n  }\n}\n\nasync function loadRuntimeAssets(\n  basePath: string,\n  exampleSource: string,\n  exampleGameSettings: string,\n  exampleRuntimeFiles: RuntimeFile[],\n) {\n  setStatus('Preparing assets...')\n\n  const assetBuffer = normalizeFileData(await fetchArrayBuffer(`${basePath}runtime/asset.zip`))\n  const sampleWasmPromise = fetchArrayBuffer(`${basePath}runtime/sample.wasm`)\n\n  setStatus('Preparing fonts...')\n  const fontEntries = {\n    'asset/font/arial.ttf': normalizeFileData(await fetchArrayBuffer(`${basePath}fonts/arial.ttf`)),\n    'asset/font/SourceHanSansSC-Regular.ttf': normalizeFileData(\n      await fetchArrayBuffer(`${basePath}fonts/SourceHanSansSC-Regular.ttf`),\n    ),\n  }\n\n  return {\n    assetBuffer,\n    fontZip: zipSync(fontEntries),\n    mainZip: buildMainZip(exampleSource, exampleGameSettings, exampleRuntimeFiles),\n    sampleWasmBuffer: normalizeFileData(await sampleWasmPromise),\n  }\n}\n\nasync function startRuntime(\n  createApp: PlayAppOptions['appFactory'],\n  basePath: string,\n  canvas: HTMLCanvasElement,\n  assets: Awaited<ReturnType<typeof loadRuntimeAssets>>,\n) {\n  setStatus('Starting Soluna app...')\n\n  return createRuntimeHandle(\n    {\n      appFactory: createApp,\n      appBaseUrl: `${basePath}runtime/`,\n      canvas,\n      print(text) {\n        appendConsole(String(text || ''), false)\n      },\n      printErr(text) {\n        appendConsole(String(text || ''), true)\n      },\n      onAbort(reason) {\n        setStatus('Runtime aborted.')\n        setNote(String(reason || 'Unknown error'))\n      },\n    },\n    {\n      arguments: [\n        'zipfile=/data/main.zip:/data/asset.zip:/data/font.zip',\n        'cpath=/data/?.wasm',\n      ],\n      files: [\n        { path: '/data/asset.zip', data: assets.assetBuffer, canOwn: true },\n        { path: '/data/main.zip', data: assets.mainZip, canOwn: true },\n        { path: '/data/font.zip', data: assets.fontZip, canOwn: true },\n        { path: '/data/sample.wasm', data: assets.sampleWasmBuffer, canOwn: true },\n      ],\n    },\n  )\n}\n\nexport default async function initPlay(options: PlayOptions): Promise<void> {\n  const basePath = import.meta.env.BASE_URL\n  const codeTarget = qs<HTMLElement>('#code-content')\n  if (codeTarget) {\n    codeTarget.textContent = options.exampleSource\n  }\n\n  setOverlayVisible(true)\n  setStatus('Loading example source...')\n  setNote('')\n  resetConsole()\n  await destroyActiveRuntime()\n\n  let createApp: PlayAppOptions['appFactory']\n  try {\n    createApp = await loadAppFactory(basePath)\n  }\n  catch (error) {\n    setStatus('Failed to load soluna.js.')\n    setNote(error instanceof Error ? error.message : String(error))\n    return\n  }\n\n  if (!(await ensureIsolation(basePath))) {\n    return\n  }\n\n  let assets: Awaited<ReturnType<typeof loadRuntimeAssets>>\n  try {\n    assets = await loadRuntimeAssets(\n      basePath,\n      options.exampleSource,\n      options.exampleGameSettings ?? '',\n      options.exampleRuntimeFiles ?? [],\n    )\n  }\n  catch (error) {\n    const message = error instanceof Error ? error.message : String(error)\n    if (message.includes('sample.wasm')) {\n      setStatus('Failed to load external module sample.wasm.')\n    }\n    else if (message.includes('/fonts/')) {\n      setStatus('Failed to load font assets.')\n    }\n    else if (message.includes('asset.zip')) {\n      setStatus('Failed to load asset archive.')\n    }\n    else {\n      setStatus('Failed to prepare runtime assets.')\n    }\n    setNote(message)\n    return\n  }\n\n  const canvas = createCanvas()\n  const resizeHandler = setupCanvasResize(canvas)\n  window.addEventListener('resize', resizeHandler)\n\n  try {\n    const runtime = await startRuntime(createApp, basePath, canvas, assets)\n    window.SOLUNA_PLAY_ACTIVE = {\n      stop() {\n        window.removeEventListener('resize', resizeHandler)\n        runtime.stop()\n        canvas.remove()\n      },\n    }\n    setOverlayVisible(false)\n  }\n  catch (error) {\n    window.removeEventListener('resize', resizeHandler)\n    canvas.remove()\n    setStatus('Failed to start runtime.')\n    setNote(error instanceof Error ? error.message : String(error))\n  }\n}\n"
  },
  {
    "path": "website/packages/astro-theme-soluna/src/components/Footer.astro",
    "content": "<footer class=\"footer\">\n  <div>Soluna Web Lab / Live Engine Reference</div>\n</footer>\n"
  },
  {
    "path": "website/packages/astro-theme-soluna/src/components/Hero.astro",
    "content": "---\ninterface Props {\n  eyebrow: string\n  title: string\n  subtitle: string\n}\n\nconst { eyebrow, title, subtitle } = Astro.props\n---\n\n<section class=\"hero\">\n  <div class=\"hero-eyebrow\">{eyebrow}</div>\n  <h1 class=\"hero-title\">{title}</h1>\n  <p class=\"hero-subtitle\">{subtitle}</p>\n</section>\n"
  },
  {
    "path": "website/packages/astro-theme-soluna/src/components/Menubar.astro",
    "content": "<div class=\"menubar\">\n  <slot />\n</div>\n"
  },
  {
    "path": "website/packages/astro-theme-soluna/src/components/Nav.astro",
    "content": "---\nconst basePath = import.meta.env.BASE_URL\n---\n\n<header class=\"nav\">\n  <div class=\"nav-inner\">\n    <a class=\"brand\" href={basePath}>\n      <span class=\"brand-mark\"></span>\n      <span>Soluna Web Lab</span>\n    </a>\n    <nav class=\"nav-links\">\n      <a href={`${basePath}docs/`}>Docs</a>\n      <a href={`${basePath}examples/`}>Gallery</a>\n      <a href=\"https://github.com/cloudwu/soluna\" rel=\"noreferrer\">GitHub</a>\n    </nav>\n  </div>\n</header>\n"
  },
  {
    "path": "website/packages/astro-theme-soluna/src/components/PlainList.astro",
    "content": "<ul class=\"plain-list\">\n  <slot />\n</ul>\n"
  },
  {
    "path": "website/packages/astro-theme-soluna/src/components/Section.astro",
    "content": "---\ninterface Props {\n  title: string\n  subtitle?: string\n  subtitleHtml?: string\n}\n\nconst { title, subtitle, subtitleHtml } = Astro.props\n---\n\n<section class=\"section\">\n  <h2 class=\"section-title\">{title}</h2>\n  {subtitleHtml ? <p class=\"section-subtitle\" set:html={subtitleHtml}></p> : subtitle ? <p class=\"section-subtitle\">{subtitle}</p> : null}\n  <slot />\n</section>\n"
  },
  {
    "path": "website/packages/astro-theme-soluna/src/components/docs/DocsPage.astro",
    "content": "---\nimport Menubar from '../Menubar.astro'\n\ninterface DocBlock {\n  signature: string | null\n  docs: string[]\n  annos: string[]\n}\n\ninterface DocModule {\n  module: string\n  title: string\n  blocks: DocBlock[]\n}\n\ninterface Props {\n  modules: DocModule[]\n}\n\nconst { modules } = Astro.props\nconst basePath = import.meta.env.BASE_URL\n\nfunction buildSegments(lines: string[]) {\n  const segments: Array<{ type: 'paragraph' | 'list', lines: string[] }> = []\n  let paragraph: string[] = []\n  let list: string[] = []\n\n  const flushParagraph = () => {\n    if (paragraph.length > 0) {\n      segments.push({ type: 'paragraph', lines: [paragraph.join(' ')] })\n      paragraph = []\n    }\n  }\n\n  const flushList = () => {\n    if (list.length > 0) {\n      segments.push({ type: 'list', lines: list })\n      list = []\n    }\n  }\n\n  for (const line of lines) {\n    if (line.startsWith('- ')) {\n      flushParagraph()\n      list.push(line.slice(2))\n    }\n else {\n      flushList()\n      paragraph.push(line)\n    }\n  }\n\n  flushParagraph()\n  flushList()\n  return segments\n}\n---\n\n<section class=\"section\">\n  <div class=\"section-header\">\n    <div>\n      <h1 class=\"section-title\">Docs</h1>\n      <p class=\"section-subtitle\">Soluna API reference.</p>\n    </div>\n  </div>\n</section>\n\n<Menubar>\n  <a href={basePath}>home</a> · <a href=\"#contents\">contents</a> · <a href=\"#index\">index</a>\n</Menubar>\n\n<section>\n  <h1 id=\"contents\"><a href=\"#contents\">Contents</a></h1>\n  <ul class=\"plain-list\">\n    {modules.map(module => (\n      <li><a href={`#${module.module}`}>{module.title}</a></li>\n    ))}\n  </ul>\n</section>\n\n<section>\n  <h1 id=\"index\"><a href=\"#index\">Index</a></h1>\n  <ul class=\"plain-list\">\n    {modules.map(module =>\n      module.blocks.map((block, index) => (\n        <li>\n          <a href={`#${module.module}-${index + 1}`}>{block.signature ?? '@block'}</a>\n        </li>\n      )),\n    )}\n  </ul>\n</section>\n\n<div class=\"docs-layout\">\n  <aside class=\"docs-nav\">\n    <div class=\"docs-nav-title\">Modules</div>\n    <div class=\"docs-nav-list\">\n      {modules.map(module => (\n        <a href={`#${module.module}`}>{module.title}</a>\n      ))}\n    </div>\n  </aside>\n\n  <div>\n    {modules.map(module => (\n      <section>\n        <h1 id={module.module}><a href={`#${module.module}`}>{module.title}</a></h1>\n        {module.module.toLowerCase() !== module.title.toLowerCase() ? <p><small>{module.module}</small></p> : null}\n        {module.blocks.map((block, index) => {\n          const blockId = `${module.module}-${index + 1}`\n          const segments = buildSegments(block.docs)\n          return (\n            <div class=\"docs-block\">\n              <h3 id={blockId}>\n                <a href={`#${blockId}`}><code>{block.signature ?? '@block'}</code></a>\n              </h3>\n              {segments.map(segment =>\n                segment.type === 'paragraph'\n? (\n                  segment.lines.map(line => <p>{line}</p>)\n                )\n: (\n                  <ul class=\"plain-list\">\n                    {segment.lines.map(line => <li>{line}</li>)}\n                  </ul>\n                ),\n              )}\n              {block.annos.length > 0\n? (\n                <pre class=\"docs-anno\"><code>{block.annos.map(anno => `@${anno}`).join('\\n')}</code></pre>\n              )\n: null}\n            </div>\n          )\n        })}\n      </section>\n    ))}\n  </div>\n</div>\n"
  },
  {
    "path": "website/packages/astro-theme-soluna/src/components/examples/ExampleListPage.astro",
    "content": "---\nimport Menubar from '../Menubar.astro'\n\ninterface ExampleItem {\n  id: string\n  title: string\n  entry: string\n}\n\ninterface Props {\n  examples: ExampleItem[]\n}\n\nconst { examples } = Astro.props\nconst basePath = import.meta.env.BASE_URL\n---\n\n<section class=\"section\">\n  <div class=\"section-header\">\n    <div>\n      <h1 class=\"section-title\">Examples</h1>\n      <p class=\"section-subtitle\">Gallery of Soluna test entries.</p>\n    </div>\n  </div>\n</section>\n\n<Menubar>\n  <a href={basePath}>home</a>\n</Menubar>\n\n<ul class=\"example-index\">\n  {examples.map(example => (\n    <li>\n      <a href={`${basePath}examples/${example.id}/`}>{example.title}</a>\n      <code>{example.entry}</code>\n    </li>\n  ))}\n</ul>\n"
  },
  {
    "path": "website/packages/astro-theme-soluna/src/components/examples/ExamplePlayPage.astro",
    "content": "---\nimport Menubar from '../Menubar.astro'\n\ninterface ExampleItem {\n  id: string\n  title: string\n  entry: string\n  source: string\n  gameSettings: string\n  runtimeFiles: Array<{\n    path: string\n    source: string\n  }>\n}\n\ninterface Props {\n  example: ExampleItem\n  examples: ExampleItem[]\n}\n\nconst { example, examples } = Astro.props\nconst basePath = import.meta.env.BASE_URL\nconst sourceJson = JSON.stringify(example.source).replace(/</g, '\\\\u003c')\nconst gameSettingsJson = JSON.stringify(example.gameSettings).replace(/</g, '\\\\u003c')\nconst runtimeFilesJson = JSON.stringify(example.runtimeFiles).replace(/</g, '\\\\u003c')\n---\n\n<section class=\"section\">\n  <div class=\"section-header\">\n    <div>\n      <h1 class=\"section-title\">{example.title}</h1>\n      <p class=\"section-subtitle\">Entry: {example.entry}</p>\n    </div>\n  </div>\n</section>\n\n<Menubar>\n  <a href={`${basePath}examples/`}>gallery</a>\n</Menubar>\n\n<div class=\"play-shell\">\n  <nav class=\"play-tabs\" aria-label=\"Example list\">\n    {examples.map(item => (\n      <a class:list={['play-tab', item.id === example.id && 'active']} href={`${basePath}examples/${item.id}/`}>{item.title}</a>\n    ))}\n  </nav>\n\n  <section class=\"runtime-panel\">\n    <div class=\"panel-header\">\n      <div>\n        <div class=\"panel-title\">Runtime Viewport</div>\n        <div class=\"panel-meta\">{example.entry}</div>\n      </div>\n    </div>\n    <div class=\"play-stage\">\n      <div id=\"soluna-stage-host\"></div>\n      <div class=\"overlay\" id=\"play-overlay\">\n        <div class=\"overlay-card\">\n          <div class=\"overlay-title\">Preparing Runtime</div>\n          <p class=\"overlay-status\" id=\"play-status\">Loading assets...</p>\n          <div class=\"note\" id=\"play-note\"></div>\n        </div>\n      </div>\n    </div>\n  </section>\n\n  <div class=\"inspector-grid\">\n    <section class=\"console-panel inspector-panel\">\n      <div class=\"panel-header\">\n        <div class=\"panel-title\">Console</div>\n      </div>\n      <pre class=\"console-body\" id=\"console-output\"></pre>\n    </section>\n\n    <section class=\"code-panel inspector-panel\">\n      <div class=\"panel-header\">\n        <div>\n          <div class=\"panel-title\">Example Source</div>\n          <div class=\"panel-meta\">main.lua</div>\n        </div>\n      </div>\n      <pre class=\"code-block\"><code id=\"code-content\">{example.source}</code></pre>\n    </section>\n  </div>\n</div>\n\n<script type=\"application/json\" id=\"soluna-example-source\" set:html={sourceJson}></script>\n<script type=\"application/json\" id=\"soluna-example-game-settings\" set:html={gameSettingsJson}></script>\n<script type=\"application/json\" id=\"soluna-example-runtime-files\" set:html={runtimeFilesJson}></script>\n<script>\nimport initPlay from 'astro-theme-soluna/client/play'\n\nconst sourceNode = document.querySelector('#soluna-example-source')\nconst gameSettingsNode = document.querySelector('#soluna-example-game-settings')\nconst runtimeFilesNode = document.querySelector('#soluna-example-runtime-files')\nconst exampleSource = sourceNode ? JSON.parse(sourceNode.textContent ?? '\"\"') : ''\nconst exampleGameSettings = gameSettingsNode ? JSON.parse(gameSettingsNode.textContent ?? '\"\"') : ''\nconst exampleRuntimeFiles = runtimeFilesNode ? JSON.parse(runtimeFilesNode.textContent ?? '[]') : []\n\ninitPlay({ exampleSource, exampleGameSettings, exampleRuntimeFiles })\n</script>\n"
  },
  {
    "path": "website/packages/astro-theme-soluna/src/layouts/BaseLayout.astro",
    "content": "---\nimport Footer from '../components/Footer.astro'\nimport Nav from '../components/Nav.astro'\nimport '../styles/theme.css'\n\ninterface Props {\n  title: string\n  description?: string\n}\n\nconst {\n  title,\n  description = 'Live documentation and browser-run examples for the Soluna 2D game engine.',\n} = Astro.props\nconst pageTitle = title === 'Soluna' ? 'Soluna' : `${title} | Soluna`\nconst siteUrl = Astro.site ?? new URL('https://cloudwu.github.io')\nconst canonicalUrl = new URL(Astro.url.pathname, siteUrl)\n---\n\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <title>{pageTitle}</title>\n    <meta name=\"description\" content={description} />\n    <link rel=\"canonical\" href={canonicalUrl.href} />\n    <meta name=\"theme-color\" content=\"#ffffff\" />\n    <meta property=\"og:site_name\" content=\"Soluna\" />\n    <meta property=\"og:type\" content=\"website\" />\n    <meta property=\"og:title\" content={pageTitle} />\n    <meta property=\"og:description\" content={description} />\n    <meta property=\"og:url\" content={canonicalUrl.href} />\n    <meta property=\"og:locale\" content=\"en_US\" />\n    <meta property=\"og:locale:alternate\" content=\"zh_CN\" />\n    <meta name=\"twitter:card\" content=\"summary\" />\n    <meta name=\"twitter:title\" content={pageTitle} />\n    <meta name=\"twitter:description\" content={description} />\n  </head>\n  <body>\n    <div class=\"page\">\n      <Nav />\n      <main>\n        <slot />\n      </main>\n      <Footer />\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "website/packages/astro-theme-soluna/src/styles/theme.css",
    "content": ":root {\n  --page-bg: #f8f8f8;\n  --paper: #ffffff;\n  --border: #cccccc;\n  --border-soft: #e2e2e2;\n  --text: #000000;\n  --muted: #444444;\n  --accent: #000080;\n  --accent-soft: #d0d0ff;\n  --danger: #8a0000;\n  --paper-width: 84em;\n  --radius: 20px;\n  --radius-small: 8px;\n  --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", monospace;\n  --sans: \"PingFang SC\", \"Hiragino Sans GB\", \"Microsoft YaHei\", \"Noto Sans CJK SC\", \"Source Han Sans SC\", Helvetica, Arial, sans-serif;\n  --heading: Verdana, Geneva, sans-serif;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nhtml {\n  background-color: var(--page-bg);\n}\n\nbody {\n  margin: 0;\n  min-height: 100vh;\n  background-color: var(--page-bg);\n  color: var(--text);\n  font-family: var(--sans);\n  line-height: 1.28;\n}\n\na {\n  color: var(--accent);\n  text-decoration: none;\n}\n\na:hover,\na:focus-visible {\n  background-color: var(--accent-soft);\n  color: var(--accent);\n  border-radius: 4px;\n  text-decoration: none;\n}\n\na:active {\n  color: #ff0000;\n}\n\np {\n  margin: 1em 0;\n}\n\nul,\nol {\n  margin: 1em 0;\n  padding-left: 40px;\n}\n\nli {\n  margin: 0.3em 0;\n}\n\nh1,\nh2,\nh3,\nh4 {\n  color: var(--accent);\n  font-family: var(--heading);\n  font-weight: normal;\n  font-style: normal;\n  text-align: left;\n}\n\nh1 {\n  margin: 0.6em 0 0.45em;\n  font-size: 28pt;\n}\n\nh2 {\n  margin: 1.25em 0 0.45em;\n  font-size: 20pt;\n}\n\nh3 {\n  margin: 1em 0 0.35em;\n  font-size: 15pt;\n}\n\nh4 {\n  margin: 1em 0 0.35em;\n  font-size: 12pt;\n}\n\ncode,\npre {\n  font-family: var(--mono);\n  font-size: 12pt;\n}\n\ncode {\n  padding: 0 0.18em;\n}\n\npre {\n  margin: 1em 0;\n  overflow: auto;\n  text-align: left;\n  white-space: pre-wrap;\n}\n\npre code {\n  padding: 0;\n}\n\nhr {\n  display: none;\n}\n\ntable hr {\n  display: block;\n  height: 1px;\n  border: 0;\n  background-color: #a0a0a0;\n  color: #a0a0a0;\n}\n\ntable {\n  border: none;\n  border-spacing: 0;\n  border-collapse: collapse;\n}\n\ntd,\nth {\n  padding: 0;\n  margin: 0;\n  text-align: left;\n  vertical-align: top;\n}\n\nimg {\n  max-width: 100%;\n  background-color: var(--paper);\n}\n\n.page {\n  min-height: 100vh;\n  display: flex;\n  flex-direction: column;\n}\n\n.nav,\n.footer {\n  display: none;\n}\n\nmain {\n  flex: 1;\n  width: 90%;\n  max-width: var(--paper-width);\n  margin: 16px auto;\n  padding: 32px;\n  border: 1px solid var(--border);\n  border-radius: var(--radius);\n  background-color: var(--paper);\n  color: var(--text);\n  line-height: 1.28;\n  text-align: justify;\n}\n\n.readme-content {\n  max-width: 72em;\n}\n\n.section {\n  margin-top: 0;\n}\n\n.section-header {\n  display: flex;\n  align-items: flex-end;\n  justify-content: space-between;\n  gap: 12px;\n  margin-bottom: 0.8em;\n}\n\n.section-title {\n  margin: 0;\n  color: var(--accent);\n  font-family: var(--heading);\n  font-size: 28pt;\n  font-weight: normal;\n}\n\n.section-subtitle {\n  max-width: 640px;\n  margin: 0.65em 0 0;\n  color: var(--muted);\n}\n\n.menubar {\n  padding-bottom: 0.5em;\n}\n\n.menubar a:hover,\n.menubar a:focus-visible {\n  margin: -3px;\n  padding: 3px;\n}\n\n.plain-list {\n  margin: 10px 0 0;\n  padding-left: 20px;\n}\n\n.plain-list li {\n  margin: 6px 0;\n}\n\n.grid {\n  display: grid;\n  gap: 10px;\n}\n\n.grid-3 {\n  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));\n}\n\n.grid-2 {\n  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));\n}\n\n.card {\n  padding: 0;\n  border: none;\n  background: transparent;\n}\n\n.card-title {\n  margin: 0 0 6px;\n  font-size: 1rem;\n}\n\n.card-meta {\n  color: var(--muted);\n  font-size: 0.8rem;\n}\n\n.card-desc {\n  margin: 8px 0 0;\n  color: var(--muted);\n}\n\n.search {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  padding: 6px 8px;\n  border: 1px solid var(--border);\n  border-radius: var(--radius-small);\n  background-color: var(--paper);\n}\n\n.search-label {\n  color: var(--muted);\n  font-size: 0.7rem;\n}\n\n.search input {\n  width: 100%;\n  border: none;\n  outline: none;\n  background: transparent;\n  color: var(--text);\n  font-size: 1rem;\n}\n\n.docs-layout {\n  display: grid;\n  grid-template-columns: minmax(180px, 220px) 1fr;\n  gap: 16px;\n  margin-top: 16px;\n}\n\n.docs-nav {\n  position: sticky;\n  top: 20px;\n  align-self: start;\n  padding: 10px;\n  border: 1px solid var(--border-soft);\n  border-radius: 12px;\n  background-color: var(--paper);\n}\n\n.docs-nav-title {\n  margin-bottom: 12px;\n  color: var(--muted);\n  font-size: 0.85rem;\n}\n\n.docs-nav-list {\n  display: flex;\n  flex-direction: column;\n  gap: 6px;\n  max-height: 70vh;\n  overflow: auto;\n}\n\n.docs-nav-list a {\n  padding: 2px 0;\n  color: var(--muted);\n}\n\n.docs-nav-list a:hover,\n.docs-nav-list a:focus-visible {\n  color: var(--accent);\n}\n\n.docs-block {\n  margin: 0 0 16px;\n  padding-left: 10px;\n  border-left: 2px solid #333333;\n}\n\n.docs-block h3 {\n  margin: 0 0 6px;\n}\n\n.docs-block h3 code {\n  font-family: inherit;\n  font-size: inherit;\n}\n\n.docs-anno {\n  margin: 1em 0;\n  padding: 0;\n  overflow-x: auto;\n  color: var(--text);\n  background: transparent;\n  font-size: 12pt;\n  line-height: 1.25;\n  white-space: pre-wrap;\n}\n\n.example-index {\n  margin: 1em 0;\n  padding: 0;\n  list-style: none;\n}\n\n.example-index li {\n  display: grid;\n  grid-template-columns: minmax(12em, 1fr) minmax(14em, 2fr);\n  gap: 12px;\n  align-items: baseline;\n  margin: 0;\n  padding: 0.35em 0;\n}\n\n.example-index code {\n  color: var(--muted);\n  font-size: 10.5pt;\n}\n\n.play-shell {\n  display: grid;\n  gap: 14px;\n}\n\n.play-tabs {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 6px;\n  padding-bottom: 4px;\n}\n\n.play-tab {\n  padding: 3px 7px;\n  border-radius: 999px;\n  color: var(--accent);\n  white-space: nowrap;\n}\n\n.play-tab.active {\n  background-color: var(--accent-soft);\n  color: var(--accent);\n}\n\n.runtime-panel,\n.inspector-panel {\n  overflow: hidden;\n  border: 1px solid var(--border-soft);\n  border-radius: 16px;\n  background-color: var(--paper);\n}\n\n.panel-header {\n  min-height: 34px;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 12px;\n  padding: 8px 12px;\n}\n\n.panel-title {\n  color: var(--accent);\n  font-family: var(--heading);\n  font-size: 0.9rem;\n}\n\n.panel-meta {\n  margin-top: 2px;\n  color: var(--muted);\n  font-family: var(--mono);\n  font-size: 0.76rem;\n}\n\n.play-stage {\n  position: relative;\n  height: clamp(500px, 66vh, 760px);\n  min-height: 420px;\n  overflow: hidden;\n  border-top: 1px solid var(--border-soft);\n  background-color: var(--paper);\n}\n\n.play-stage canvas {\n  width: 100%;\n  height: 100%;\n  display: block;\n}\n\n#soluna-stage-host {\n  width: 100%;\n  height: 100%;\n  display: block;\n}\n\n.inspector-grid {\n  display: grid;\n  grid-template-columns: minmax(280px, 0.8fr) minmax(360px, 1.2fr);\n  gap: 14px;\n  align-items: start;\n}\n\n.console-body,\n.code-block {\n  margin: 0;\n  padding: 0 12px 12px;\n  overflow: auto;\n  background-color: var(--paper);\n  color: var(--text);\n  font-family: var(--mono);\n  font-size: 10.5pt;\n  line-height: 1.35;\n  overflow-wrap: anywhere;\n  text-align: left;\n  white-space: pre-wrap;\n}\n\n.console-body {\n  min-height: 13em;\n  max-height: 22em;\n}\n\n.console-body:empty::before {\n  content: \"No output.\";\n  color: var(--muted);\n}\n\n.console-error {\n  color: var(--danger);\n}\n\n.code-block {\n  max-height: 38em;\n}\n\n.code-block code {\n  padding: 0;\n}\n\n.overlay {\n  position: absolute;\n  inset: 0;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: 24px;\n  background-color: rgba(255, 255, 255, 0.9);\n  color: var(--text);\n  text-align: center;\n}\n\n.overlay.hidden {\n  opacity: 0;\n  pointer-events: none;\n}\n\n.overlay-card {\n  max-width: 420px;\n  padding: 16px 18px;\n  border: 1px solid var(--border-soft);\n  border-radius: 16px;\n  background-color: var(--paper);\n}\n\n.overlay-title {\n  margin: 0 0 10px;\n  color: var(--accent);\n  font-family: var(--heading);\n  font-size: 1.45rem;\n}\n\n.overlay-status {\n  margin: 0;\n  color: var(--muted);\n  line-height: 1.6;\n}\n\n.note {\n  margin-top: 8px;\n  color: var(--danger);\n  font-size: 0.85rem;\n}\n\n:target {\n  margin: -8px;\n  padding: 8px;\n  border-radius: var(--radius-small);\n  background-color: #f0f0f0;\n  outline: none;\n}\n\n.is-hidden {\n  display: none !important;\n}\n\n@media (max-width: 960px) {\n  .docs-layout,\n  .inspector-grid {\n    grid-template-columns: 1fr;\n  }\n\n  .docs-nav {\n    position: static;\n  }\n}\n\n@media (max-width: 640px) {\n  main {\n    width: calc(100% - 16px);\n    margin: 8px auto;\n    padding: 16px 12px 40px;\n    border-radius: 16px;\n  }\n\n  h1,\n  .section-title {\n    font-size: 22pt;\n  }\n\n  .section-header {\n    align-items: flex-start;\n    flex-direction: column;\n  }\n\n  .example-index li {\n    grid-template-columns: 1fr;\n    gap: 4px;\n  }\n\n  .play-stage {\n    height: 58vh;\n    min-height: 320px;\n  }\n}\n"
  },
  {
    "path": "website/pnpm-workspace.yaml",
    "content": "packages:\n  - packages/*\n"
  },
  {
    "path": "website/scripts/prepare-runtime.mjs",
    "content": "import { execFile } from 'node:child_process'\nimport { copyFile, mkdir, rm, stat } from 'node:fs/promises'\nimport path from 'node:path'\nimport process from 'node:process'\nimport { fileURLToPath } from 'node:url'\nimport { promisify } from 'node:util'\n\nconst execFileAsync = promisify(execFile)\n\nconst websiteDir = path.resolve(fileURLToPath(new URL('..', import.meta.url)))\nconst rootDir = path.resolve(websiteDir, '..')\nconst websiteRuntimeDir = path.join(websiteDir, 'public', 'runtime')\n\nfunction resolveMode() {\n  return process.env.SOLUNA_MODE || 'release'\n}\n\nfunction resolveRuntimePath(name, fallback) {\n  const configuredPath = process.env[name]\n  if (!configuredPath) {\n    return fallback\n  }\n  if (path.isAbsolute(configuredPath)) {\n    return configuredPath\n  }\n  return path.resolve(rootDir, configuredPath)\n}\n\nfunction exists(filePath) {\n  return stat(filePath).then(() => true, () => false)\n}\n\nasync function ensureFile(sourcePath, label) {\n  if (!(await exists(sourcePath))) {\n    throw new Error(`Missing ${label}: ${sourcePath}`)\n  }\n}\n\nasync function createAssetZip(outputPath) {\n  await execFileAsync('zip', ['-qr', outputPath, 'asset'], {\n    cwd: rootDir,\n  })\n}\n\nasync function main() {\n  const mode = resolveMode()\n  const solunaJsPath = resolveRuntimePath('SOLUNA_JS_PATH', path.join(rootDir, 'bin', 'emcc', mode, 'soluna.js'))\n  const solunaWasmPath = resolveRuntimePath('SOLUNA_WASM_PATH', path.join(rootDir, 'bin', 'emcc', mode, 'soluna.wasm'))\n  const sampleWasmPath = resolveRuntimePath('SAMPLE_WASM_PATH', path.join(rootDir, 'bin', 'emcc', 'release', 'sample.wasm'))\n  const solunaWasmMapPath = resolveRuntimePath(\n    'SOLUNA_WASM_MAP_PATH',\n    path.join(rootDir, 'bin', 'emcc', mode, 'soluna.wasm.map'),\n  )\n\n  await ensureFile(solunaJsPath, 'soluna.js')\n  await ensureFile(solunaWasmPath, 'soluna.wasm')\n  await ensureFile(sampleWasmPath, 'sample.wasm')\n\n  await rm(websiteRuntimeDir, { recursive: true, force: true })\n  await mkdir(websiteRuntimeDir, { recursive: true })\n\n  await copyFile(solunaJsPath, path.join(websiteRuntimeDir, 'soluna.js'))\n  await copyFile(solunaWasmPath, path.join(websiteRuntimeDir, 'soluna.wasm'))\n  await copyFile(sampleWasmPath, path.join(websiteRuntimeDir, 'sample.wasm'))\n\n  if (await exists(solunaWasmMapPath)) {\n    await copyFile(solunaWasmMapPath, path.join(websiteRuntimeDir, 'soluna.wasm.map'))\n  }\n\n  await createAssetZip(path.join(websiteRuntimeDir, 'asset.zip'))\n\n  process.stdout.write(`Prepared website runtime in ${websiteRuntimeDir}\\n`)\n}\n\nmain().catch((error) => {\n  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`)\n  process.exitCode = 1\n})\n"
  },
  {
    "path": "website/src/content.config.ts",
    "content": "import { defineCollection, z } from 'astro:content'\nimport { loadDocs, loadExamples } from './lib/content'\n\nconst examples = defineCollection({\n  loader: async () => loadExamples(),\n  schema: z.object({\n    title: z.string(),\n    entry: z.string(),\n    source: z.string(),\n    gameSettings: z.string(),\n    runtimeFiles: z.array(z.object({\n      path: z.string(),\n      source: z.string(),\n    })),\n  }),\n})\n\nconst docs = defineCollection({\n  loader: async () => loadDocs(),\n  schema: z.object({\n    module: z.string(),\n    title: z.string(),\n    blocks: z.array(\n      z.object({\n        signature: z.string().nullable(),\n        docs: z.array(z.string()),\n        annos: z.array(z.string()),\n      }),\n    ),\n  }),\n})\n\nexport const collections = {\n  docs,\n  examples,\n}\n"
  },
  {
    "path": "website/src/lib/content.ts",
    "content": "import { readdir, readFile, stat } from 'node:fs/promises'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nexport interface ExampleRuntimeFile {\n  path: string\n  source: string\n}\n\nexport interface ExampleEntry {\n  id: string\n  title: string\n  entry: string\n  source: string\n  gameSettings: string\n  runtimeFiles: ExampleRuntimeFile[]\n}\n\nexport interface DocBlock {\n  signature: string | null\n  docs: string[]\n  annos: string[]\n}\n\nexport interface DocEntry {\n  id: string\n  module: string\n  title: string\n  blocks: DocBlock[]\n}\n\nconst repoRoot = path.resolve(fileURLToPath(new URL('../../..', import.meta.url)))\nconst testDir = path.join(repoRoot, 'test')\nconst docsDir = path.join(repoRoot, 'docs')\n\nfunction trim(value: string): string {\n  return value.trim()\n}\n\nexport function titleize(name: string): string {\n  return name\n    .split(/[_\\-\\s]+/)\n    .filter(Boolean)\n    .map(part => part.slice(0, 1).toUpperCase() + part.slice(1))\n    .join(' ')\n}\n\nasync function exists(filePath: string): Promise<boolean> {\n  return stat(filePath).then(() => true, () => false)\n}\n\nfunction normalizeGameSettings(source: string): string {\n  const skippedKeys = new Set(['entry', 'extlua_entry', 'extlua_preload'])\n  return source\n    .split(/\\r?\\n/)\n    .filter((line) => {\n      const match = line.match(/^\\s*([a-z_][\\w.]*)\\s*:/i)\n      return !match || !skippedKeys.has(match[1])\n    })\n    .join('\\n')\n    .trim()\n}\n\nasync function loadGameSettings(id: string): Promise<string> {\n  const filename = path.join(testDir, `${id}.game`)\n  if (!(await exists(filename))) {\n    return ''\n  }\n  return normalizeGameSettings(await readFile(filename, 'utf8'))\n}\n\nasync function loadRuntimeFiles(root: string, prefix: string): Promise<ExampleRuntimeFile[]> {\n  if (!(await exists(root))) {\n    return []\n  }\n\n  const entries = await readdir(root, { withFileTypes: true })\n  const files = await Promise.all(entries.map(async (entry) => {\n    const fullPath = path.join(root, entry.name)\n    const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name\n    if (entry.isDirectory()) {\n      return loadRuntimeFiles(fullPath, relativePath)\n    }\n    if (!entry.isFile()) {\n      return []\n    }\n    return [{\n      path: relativePath,\n      source: await readFile(fullPath, 'utf8'),\n    }]\n  }))\n\n  return files.flat().sort((a, b) => a.path.localeCompare(b.path))\n}\n\nexport async function loadExamples(): Promise<ExampleEntry[]> {\n  const names = (await readdir(testDir))\n    .filter(name => name.endsWith('.lua'))\n    .sort()\n\n  const examples = await Promise.all(\n    names.map(async (name) => {\n      const id = name.slice(0, -4)\n      const source = await readFile(path.join(testDir, name), 'utf8')\n      const gameSettings = await loadGameSettings(id)\n      const runtimeFiles = await loadRuntimeFiles(path.join(testDir, id), id)\n      return {\n        id,\n        title: titleize(id),\n        entry: `test/${name}`,\n        source,\n        gameSettings,\n        runtimeFiles,\n      }\n    }),\n  )\n\n  return examples\n}\n\nexport async function loadDocs(): Promise<DocEntry[]> {\n  const names = (await readdir(docsDir))\n    .filter(name => name.endsWith('.lua'))\n    .sort()\n\n  const modules = await Promise.all(\n    names.map(async (name) => {\n      const moduleName = name.slice(0, -4)\n      const fileContent = await readFile(path.join(docsDir, name), 'utf8')\n      return {\n        id: moduleName,\n        module: moduleName,\n        title: titleize(moduleName),\n        blocks: parseDocFile(fileContent),\n      }\n    }),\n  )\n\n  return modules\n}\n\nfunction parseDocFile(content: string): DocBlock[] {\n  const blocks: DocBlock[] = []\n  let docLines: string[] = []\n  let annos: string[] = []\n\n  const flush = (signature: string | null) => {\n    if (docLines.length === 0 && annos.every(anno => anno === 'meta' || anno.startsWith('meta '))) {\n      docLines = []\n      annos = []\n      return\n    }\n    if (docLines.length === 0 && annos.length === 0) {\n      return\n    }\n    blocks.push({\n      signature: signature ?? annotationSignature(annos),\n      docs: docLines,\n      annos,\n    })\n    docLines = []\n    annos = []\n  }\n\n  for (const line of content.split(/\\r?\\n/)) {\n    if (line.startsWith('---@')) {\n      annos.push(trim(line.replace(/^---@/, '')))\n      continue\n    }\n    if (line.startsWith('---')) {\n      docLines.push(trim(line.replace(/^---\\s?/, '')))\n      continue\n    }\n\n    const trimmed = trim(line)\n    if (docLines.length > 0 || annos.length > 0) {\n      if (trimmed === '') {\n        flush(null)\n        continue\n      }\n      flush(trimmed)\n    }\n  }\n\n  flush(null)\n  return blocks\n}\n\nfunction annotationSignature(annos: string[]): string | null {\n  const anno = annos.find(anno =>\n    anno.startsWith('alias ')\n    || anno.startsWith('class ')\n    || anno.startsWith('type ')\n    || anno.startsWith('field '),\n  )\n  return anno ? `@${anno}` : null\n}\n"
  },
  {
    "path": "website/src/lib/readme.ts",
    "content": "import { readFile } from 'node:fs/promises'\nimport { marked } from 'marked'\n\nconst readmeUrl = new URL('../../../README.md', import.meta.url)\nconst repoUrl = 'https://github.com/cloudwu/soluna'\n\nfunction normalizeGithubPath(pathname: string): string {\n  if (pathname.startsWith('./')) {\n    return pathname.slice(2)\n  }\n  if (pathname.startsWith('/../../')) {\n    return pathname.slice('/../../'.length)\n  }\n  return pathname\n}\n\nfunction resolveReadmeHref(href: string, basePath: string): string {\n  if (\n    href.startsWith('http://')\n    || href.startsWith('https://')\n    || href.startsWith('#')\n    || href.startsWith('mailto:')\n  ) {\n    return href\n  }\n\n  if (href === './docs') {\n    return `${basePath}docs/`\n  }\n  if (href === './test') {\n    return `${basePath}examples/`\n  }\n\n  const normalized = normalizeGithubPath(href)\n  if (normalized === 'LICENSE') {\n    return `${repoUrl}/blob/master/LICENSE`\n  }\n  if (normalized.startsWith('.github/') || normalized.startsWith('docs/')) {\n    return `${repoUrl}/tree/master/${normalized}`\n  }\n  if (normalized.startsWith('actions/') || normalized.startsWith('releases/')) {\n    return `${repoUrl}/${normalized}`\n  }\n\n  return href\n}\n\nfunction rewriteReadmeLinks(markdown: string, basePath: string): string {\n  return markdown.replace(/(!?\\[[^\\]]*\\])\\(([^)]+)\\)/g, (_match, label, href) => {\n    return `${label}(${resolveReadmeHref(href, basePath)})`\n  })\n}\n\nexport async function renderReadme(basePath: string): Promise<string> {\n  const readme = await readFile(readmeUrl, 'utf8')\n  const rewritten = rewriteReadmeLinks(readme, basePath)\n  return marked.parse(rewritten) as string\n}\n"
  },
  {
    "path": "website/src/pages/docs/index.astro",
    "content": "---\nimport DocsPage from 'astro-theme-soluna/components/docs/DocsPage.astro'\nimport BaseLayout from 'astro-theme-soluna/layouts/BaseLayout.astro'\nimport { getCollection } from 'astro:content'\n\nconst modules = (await getCollection('docs')).map(entry => entry.data)\n---\n\n<BaseLayout title=\"Docs\" description=\"Soluna API reference.\">\n  <DocsPage modules={modules} />\n</BaseLayout>\n"
  },
  {
    "path": "website/src/pages/examples/[id].astro",
    "content": "---\nimport ExamplePlayPage from 'astro-theme-soluna/components/examples/ExamplePlayPage.astro'\nimport BaseLayout from 'astro-theme-soluna/layouts/BaseLayout.astro'\nimport { getCollection } from 'astro:content'\n\nconst examples = (await getCollection('examples'))\n  .map(entry => ({\n    id: entry.id,\n    ...entry.data,\n  }))\n  .sort((left, right) => left.id.localeCompare(right.id))\n\nexport async function getStaticPaths() {\n  const entries = await getCollection('examples')\n  return entries.map(entry => ({\n    params: { id: entry.id },\n    props: {\n      example: {\n        id: entry.id,\n        ...entry.data,\n      },\n    },\n  }))\n}\n\nconst { example } = Astro.props\n---\n\n<BaseLayout title={example.title} description={`Soluna example: ${example.entry}`}>\n  <ExamplePlayPage example={example} examples={examples} />\n</BaseLayout>\n"
  },
  {
    "path": "website/src/pages/examples/index.astro",
    "content": "---\nimport ExampleListPage from 'astro-theme-soluna/components/examples/ExampleListPage.astro'\nimport BaseLayout from 'astro-theme-soluna/layouts/BaseLayout.astro'\nimport { getCollection } from 'astro:content'\n\nconst examples = (await getCollection('examples'))\n  .map(entry => ({\n    id: entry.id,\n    ...entry.data,\n  }))\n  .sort((left, right) => left.id.localeCompare(right.id))\n---\n\n<BaseLayout title=\"Examples\" description=\"Gallery of Soluna test entries.\">\n  <ExampleListPage examples={examples} />\n</BaseLayout>\n"
  },
  {
    "path": "website/src/pages/index.astro",
    "content": "---\nimport BaseLayout from 'astro-theme-soluna/layouts/BaseLayout.astro'\nimport { renderReadme } from '../lib/readme'\n\nconst html = await renderReadme(import.meta.env.BASE_URL)\n---\n\n<BaseLayout title=\"Soluna\" description=\"Live docs and examples for the Soluna engine.\">\n  <article class=\"readme-content\" set:html={html}></article>\n</BaseLayout>\n"
  },
  {
    "path": "website/tsconfig.json",
    "content": "{\n  \"extends\": \"astro/tsconfigs/strict\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\"\n  }\n}\n"
  }
]